Σ(゚Д゚;≡;゚д゚)Σ(゚Д゚;≡;゚д゚)Σ(゚Д゚;≡;゚д゚)始,故人唐宰相鲁公,🆚开府南服,余以布衣从戎。明年,别公漳水湄。后明年,公以事过张睢阳庙及颜杲卿所尝往来处,悲歌慷慨,卒不负其言而从之游。今其诗具在,可考也。😭 HEX
HEX
Server: Apache
System: Linux webm006.cluster120.gra.hosting.ovh.net 6.18.39-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Tue Jul 21 12:03:15 CEST 2026 x86_64
User: studionolh (122383)
PHP: 7.3.33
Disabled: _dyuweyrj4,_dyuweyrj4r,dl
Upload Files
File: /home/studionolh/www/wp-includes/Requests/Cookie/reusable-blocks/src.tar
Crypto32.php000064400000153517152331075510006720 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Crypto32', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Crypto
 *
 * ATTENTION!
 *
 * If you are using this library, you should be using
 * ParagonIE_Sodium_Compat in your code, not this class.
 */
abstract class ParagonIE_Sodium_Crypto32
{
    const aead_chacha20poly1305_KEYBYTES = 32;
    const aead_chacha20poly1305_NSECBYTES = 0;
    const aead_chacha20poly1305_NPUBBYTES = 8;
    const aead_chacha20poly1305_ABYTES = 16;

    const aead_chacha20poly1305_IETF_KEYBYTES = 32;
    const aead_chacha20poly1305_IETF_NSECBYTES = 0;
    const aead_chacha20poly1305_IETF_NPUBBYTES = 12;
    const aead_chacha20poly1305_IETF_ABYTES = 16;

    const aead_xchacha20poly1305_IETF_KEYBYTES = 32;
    const aead_xchacha20poly1305_IETF_NSECBYTES = 0;
    const aead_xchacha20poly1305_IETF_NPUBBYTES = 24;
    const aead_xchacha20poly1305_IETF_ABYTES = 16;

    const box_curve25519xsalsa20poly1305_SEEDBYTES = 32;
    const box_curve25519xsalsa20poly1305_PUBLICKEYBYTES = 32;
    const box_curve25519xsalsa20poly1305_SECRETKEYBYTES = 32;
    const box_curve25519xsalsa20poly1305_BEFORENMBYTES = 32;
    const box_curve25519xsalsa20poly1305_NONCEBYTES = 24;
    const box_curve25519xsalsa20poly1305_MACBYTES = 16;
    const box_curve25519xsalsa20poly1305_BOXZEROBYTES = 16;
    const box_curve25519xsalsa20poly1305_ZEROBYTES = 32;

    const onetimeauth_poly1305_BYTES = 16;
    const onetimeauth_poly1305_KEYBYTES = 32;

    const secretbox_xsalsa20poly1305_KEYBYTES = 32;
    const secretbox_xsalsa20poly1305_NONCEBYTES = 24;
    const secretbox_xsalsa20poly1305_MACBYTES = 16;
    const secretbox_xsalsa20poly1305_BOXZEROBYTES = 16;
    const secretbox_xsalsa20poly1305_ZEROBYTES = 32;

    const secretbox_xchacha20poly1305_KEYBYTES = 32;
    const secretbox_xchacha20poly1305_NONCEBYTES = 24;
    const secretbox_xchacha20poly1305_MACBYTES = 16;
    const secretbox_xchacha20poly1305_BOXZEROBYTES = 16;
    const secretbox_xchacha20poly1305_ZEROBYTES = 32;

    const stream_salsa20_KEYBYTES = 32;

    /**
     * AEAD Decryption with ChaCha20-Poly1305
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $ad
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function aead_chacha20poly1305_decrypt(
        $message = '',
        $ad = '',
        $nonce = '',
        $key = ''
    ) {
        /** @var int $len - Length of message (ciphertext + MAC) */
        $len = ParagonIE_Sodium_Core32_Util::strlen($message);

        /** @var int  $clen - Length of ciphertext */
        $clen = $len - self::aead_chacha20poly1305_ABYTES;

        /** @var int $adlen - Length of associated data */
        $adlen = ParagonIE_Sodium_Core32_Util::strlen($ad);

        /** @var string $mac - Message authentication code */
        $mac = ParagonIE_Sodium_Core32_Util::substr(
            $message,
            $clen,
            self::aead_chacha20poly1305_ABYTES
        );

        /** @var string $ciphertext - The encrypted message (sans MAC) */
        $ciphertext = ParagonIE_Sodium_Core32_Util::substr($message, 0, $clen);

        /** @var string The first block of the chacha20 keystream, used as a poly1305 key */
        $block0 = ParagonIE_Sodium_Core32_ChaCha20::stream(
            32,
            $nonce,
            $key
        );

        /* Recalculate the Poly1305 authentication tag (MAC): */
        $state = new ParagonIE_Sodium_Core32_Poly1305_State($block0);
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
        } catch (SodiumException $ex) {
            $block0 = null;
        }
        $state->update($ad);
        $state->update(ParagonIE_Sodium_Core32_Util::store64_le($adlen));
        $state->update($ciphertext);
        $state->update(ParagonIE_Sodium_Core32_Util::store64_le($clen));
        $computed_mac = $state->finish();

        /* Compare the given MAC with the recalculated MAC: */
        if (!ParagonIE_Sodium_Core32_Util::verify_16($computed_mac, $mac)) {
            throw new SodiumException('Invalid MAC');
        }

        // Here, we know that the MAC is valid, so we decrypt and return the plaintext
        return ParagonIE_Sodium_Core32_ChaCha20::streamXorIc(
            $ciphertext,
            $nonce,
            $key,
            ParagonIE_Sodium_Core32_Util::store64_le(1)
        );
    }

    /**
     * AEAD Encryption with ChaCha20-Poly1305
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $ad
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function aead_chacha20poly1305_encrypt(
        $message = '',
        $ad = '',
        $nonce = '',
        $key = ''
    ) {
        /** @var int $len - Length of the plaintext message */
        $len = ParagonIE_Sodium_Core32_Util::strlen($message);

        /** @var int $adlen - Length of the associated data */
        $adlen = ParagonIE_Sodium_Core32_Util::strlen($ad);

        /** @var string The first block of the chacha20 keystream, used as a poly1305 key */
        $block0 = ParagonIE_Sodium_Core32_ChaCha20::stream(
            32,
            $nonce,
            $key
        );
        $state = new ParagonIE_Sodium_Core32_Poly1305_State($block0);
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
        } catch (SodiumException $ex) {
            $block0 = null;
        }

        /** @var string $ciphertext - Raw encrypted data */
        $ciphertext = ParagonIE_Sodium_Core32_ChaCha20::streamXorIc(
            $message,
            $nonce,
            $key,
            ParagonIE_Sodium_Core32_Util::store64_le(1)
        );

        $state->update($ad);
        $state->update(ParagonIE_Sodium_Core32_Util::store64_le($adlen));
        $state->update($ciphertext);
        $state->update(ParagonIE_Sodium_Core32_Util::store64_le($len));
        return $ciphertext . $state->finish();
    }

    /**
     * AEAD Decryption with ChaCha20-Poly1305, IETF mode (96-bit nonce)
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $ad
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function aead_chacha20poly1305_ietf_decrypt(
        $message = '',
        $ad = '',
        $nonce = '',
        $key = ''
    ) {
        /** @var int $adlen - Length of associated data */
        $adlen = ParagonIE_Sodium_Core32_Util::strlen($ad);

        /** @var int $len - Length of message (ciphertext + MAC) */
        $len = ParagonIE_Sodium_Core32_Util::strlen($message);

        /** @var int  $clen - Length of ciphertext */
        $clen = $len - self::aead_chacha20poly1305_IETF_ABYTES;

        /** @var string The first block of the chacha20 keystream, used as a poly1305 key */
        $block0 = ParagonIE_Sodium_Core32_ChaCha20::ietfStream(
            32,
            $nonce,
            $key
        );

        /** @var string $mac - Message authentication code */
        $mac = ParagonIE_Sodium_Core32_Util::substr(
            $message,
            $len - self::aead_chacha20poly1305_IETF_ABYTES,
            self::aead_chacha20poly1305_IETF_ABYTES
        );

        /** @var string $ciphertext - The encrypted message (sans MAC) */
        $ciphertext = ParagonIE_Sodium_Core32_Util::substr(
            $message,
            0,
            $len - self::aead_chacha20poly1305_IETF_ABYTES
        );

        /* Recalculate the Poly1305 authentication tag (MAC): */
        $state = new ParagonIE_Sodium_Core32_Poly1305_State($block0);
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
        } catch (SodiumException $ex) {
            $block0 = null;
        }
        $state->update($ad);
        $state->update(str_repeat("\x00", ((0x10 - $adlen) & 0xf)));
        $state->update($ciphertext);
        $state->update(str_repeat("\x00", (0x10 - $clen) & 0xf));
        $state->update(ParagonIE_Sodium_Core32_Util::store64_le($adlen));
        $state->update(ParagonIE_Sodium_Core32_Util::store64_le($clen));
        $computed_mac = $state->finish();

        /* Compare the given MAC with the recalculated MAC: */
        if (!ParagonIE_Sodium_Core32_Util::verify_16($computed_mac, $mac)) {
            throw new SodiumException('Invalid MAC');
        }

        // Here, we know that the MAC is valid, so we decrypt and return the plaintext
        return ParagonIE_Sodium_Core32_ChaCha20::ietfStreamXorIc(
            $ciphertext,
            $nonce,
            $key,
            ParagonIE_Sodium_Core32_Util::store64_le(1)
        );
    }

    /**
     * AEAD Encryption with ChaCha20-Poly1305, IETF mode (96-bit nonce)
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $ad
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function aead_chacha20poly1305_ietf_encrypt(
        $message = '',
        $ad = '',
        $nonce = '',
        $key = ''
    ) {
        /** @var int $len - Length of the plaintext message */
        $len = ParagonIE_Sodium_Core32_Util::strlen($message);

        /** @var int $adlen - Length of the associated data */
        $adlen = ParagonIE_Sodium_Core32_Util::strlen($ad);

        /** @var string The first block of the chacha20 keystream, used as a poly1305 key */
        $block0 = ParagonIE_Sodium_Core32_ChaCha20::ietfStream(
            32,
            $nonce,
            $key
        );
        $state = new ParagonIE_Sodium_Core32_Poly1305_State($block0);
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
        } catch (SodiumException $ex) {
            $block0 = null;
        }

        /** @var string $ciphertext - Raw encrypted data */
        $ciphertext = ParagonIE_Sodium_Core32_ChaCha20::ietfStreamXorIc(
            $message,
            $nonce,
            $key,
            ParagonIE_Sodium_Core32_Util::store64_le(1)
        );

        $state->update($ad);
        $state->update(str_repeat("\x00", ((0x10 - $adlen) & 0xf)));
        $state->update($ciphertext);
        $state->update(str_repeat("\x00", ((0x10 - $len) & 0xf)));
        $state->update(ParagonIE_Sodium_Core32_Util::store64_le($adlen));
        $state->update(ParagonIE_Sodium_Core32_Util::store64_le($len));
        return $ciphertext . $state->finish();
    }

    /**
     * AEAD Decryption with ChaCha20-Poly1305, IETF mode (96-bit nonce)
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $ad
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function aead_xchacha20poly1305_ietf_decrypt(
        $message = '',
        $ad = '',
        $nonce = '',
        $key = ''
    ) {
        $subkey = ParagonIE_Sodium_Core32_HChaCha20::hChaCha20(
            ParagonIE_Sodium_Core32_Util::substr($nonce, 0, 16),
            $key
        );
        $nonceLast = "\x00\x00\x00\x00" .
            ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8);

        return self::aead_chacha20poly1305_ietf_decrypt($message, $ad, $nonceLast, $subkey);
    }

    /**
     * AEAD Encryption with ChaCha20-Poly1305, IETF mode (96-bit nonce)
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $ad
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function aead_xchacha20poly1305_ietf_encrypt(
        $message = '',
        $ad = '',
        $nonce = '',
        $key = ''
    ) {
        $subkey = ParagonIE_Sodium_Core32_HChaCha20::hChaCha20(
            ParagonIE_Sodium_Core32_Util::substr($nonce, 0, 16),
            $key
        );
        $nonceLast = "\x00\x00\x00\x00" .
            ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8);

        return self::aead_chacha20poly1305_ietf_encrypt($message, $ad, $nonceLast, $subkey);
    }

    /**
     * HMAC-SHA-512-256 (a.k.a. the leftmost 256 bits of HMAC-SHA-512)
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $key
     * @return string
     * @throws TypeError
     */
    public static function auth($message, $key)
    {
        return ParagonIE_Sodium_Core32_Util::substr(
            hash_hmac('sha512', $message, $key, true),
            0,
            32
        );
    }

    /**
     * HMAC-SHA-512-256 validation. Constant-time via hash_equals().
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $mac
     * @param string $message
     * @param string $key
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function auth_verify($mac, $message, $key)
    {
        return ParagonIE_Sodium_Core32_Util::hashEquals(
            $mac,
            self::auth($message, $key)
        );
    }

    /**
     * X25519 key exchange followed by XSalsa20Poly1305 symmetric encryption
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $plaintext
     * @param string $nonce
     * @param string $keypair
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box($plaintext, $nonce, $keypair)
    {
        return self::secretbox(
            $plaintext,
            $nonce,
            self::box_beforenm(
                self::box_secretkey($keypair),
                self::box_publickey($keypair)
            )
        );
    }

    /**
     * X25519-XSalsa20-Poly1305 with one ephemeral X25519 keypair.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $publicKey
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_seal($message, $publicKey)
    {
        /** @var string $ephemeralKeypair */
        $ephemeralKeypair = self::box_keypair();

        /** @var string $ephemeralSK */
        $ephemeralSK = self::box_secretkey($ephemeralKeypair);

        /** @var string $ephemeralPK */
        $ephemeralPK = self::box_publickey($ephemeralKeypair);

        /** @var string $nonce */
        $nonce = self::generichash(
            $ephemeralPK . $publicKey,
            '',
            24
        );

        /** @var string $keypair - The combined keypair used in crypto_box() */
        $keypair = self::box_keypair_from_secretkey_and_publickey($ephemeralSK, $publicKey);

        /** @var string $ciphertext Ciphertext + MAC from crypto_box */
        $ciphertext = self::box($message, $nonce, $keypair);
        try {
            ParagonIE_Sodium_Compat::memzero($ephemeralKeypair);
            ParagonIE_Sodium_Compat::memzero($ephemeralSK);
            ParagonIE_Sodium_Compat::memzero($nonce);
        } catch (SodiumException $ex) {
            $ephemeralKeypair = null;
            $ephemeralSK = null;
            $nonce = null;
        }
        return $ephemeralPK . $ciphertext;
    }

    /**
     * Opens a message encrypted via box_seal().
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $keypair
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_seal_open($message, $keypair)
    {
        /** @var string $ephemeralPK */
        $ephemeralPK = ParagonIE_Sodium_Core32_Util::substr($message, 0, 32);

        /** @var string $ciphertext (ciphertext + MAC) */
        $ciphertext = ParagonIE_Sodium_Core32_Util::substr($message, 32);

        /** @var string $secretKey */
        $secretKey = self::box_secretkey($keypair);

        /** @var string $publicKey */
        $publicKey = self::box_publickey($keypair);

        /** @var string $nonce */
        $nonce = self::generichash(
            $ephemeralPK . $publicKey,
            '',
            24
        );

        /** @var string $keypair */
        $keypair = self::box_keypair_from_secretkey_and_publickey($secretKey, $ephemeralPK);

        /** @var string $m */
        $m = self::box_open($ciphertext, $nonce, $keypair);
        try {
            ParagonIE_Sodium_Compat::memzero($secretKey);
            ParagonIE_Sodium_Compat::memzero($ephemeralPK);
            ParagonIE_Sodium_Compat::memzero($nonce);
        } catch (SodiumException $ex) {
            $secretKey = null;
            $ephemeralPK = null;
            $nonce = null;
        }
        return $m;
    }

    /**
     * Used by crypto_box() to get the crypto_secretbox() key.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $sk
     * @param string $pk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_beforenm($sk, $pk)
    {
        return ParagonIE_Sodium_Core32_HSalsa20::hsalsa20(
            str_repeat("\x00", 16),
            self::scalarmult($sk, $pk)
        );
    }

    /**
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @return string
     * @throws Exception
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_keypair()
    {
        $sKey = random_bytes(32);
        $pKey = self::scalarmult_base($sKey);
        return $sKey . $pKey;
    }

    /**
     * @param string $seed
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_seed_keypair($seed)
    {
        $sKey = ParagonIE_Sodium_Core32_Util::substr(
            hash('sha512', $seed, true),
            0,
            32
        );
        $pKey = self::scalarmult_base($sKey);
        return $sKey . $pKey;
    }

    /**
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $sKey
     * @param string $pKey
     * @return string
     * @throws TypeError
     */
    public static function box_keypair_from_secretkey_and_publickey($sKey, $pKey)
    {
        return ParagonIE_Sodium_Core32_Util::substr($sKey, 0, 32) .
            ParagonIE_Sodium_Core32_Util::substr($pKey, 0, 32);
    }

    /**
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $keypair
     * @return string
     * @throws RangeException
     * @throws TypeError
     */
    public static function box_secretkey($keypair)
    {
        if (ParagonIE_Sodium_Core32_Util::strlen($keypair) !== 64) {
            throw new RangeException(
                'Must be ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES bytes long.'
            );
        }
        return ParagonIE_Sodium_Core32_Util::substr($keypair, 0, 32);
    }

    /**
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $keypair
     * @return string
     * @throws RangeException
     * @throws TypeError
     */
    public static function box_publickey($keypair)
    {
        if (ParagonIE_Sodium_Core32_Util::strlen($keypair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) {
            throw new RangeException(
                'Must be ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES bytes long.'
            );
        }
        return ParagonIE_Sodium_Core32_Util::substr($keypair, 32, 32);
    }

    /**
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $sKey
     * @return string
     * @throws RangeException
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_publickey_from_secretkey($sKey)
    {
        if (ParagonIE_Sodium_Core32_Util::strlen($sKey) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_SECRETKEYBYTES) {
            throw new RangeException(
                'Must be ParagonIE_Sodium_Compat::CRYPTO_BOX_SECRETKEYBYTES bytes long.'
            );
        }
        return self::scalarmult_base($sKey);
    }

    /**
     * Decrypt a message encrypted with box().
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $ciphertext
     * @param string $nonce
     * @param string $keypair
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_open($ciphertext, $nonce, $keypair)
    {
        return self::secretbox_open(
            $ciphertext,
            $nonce,
            self::box_beforenm(
                self::box_secretkey($keypair),
                self::box_publickey($keypair)
            )
        );
    }

    /**
     * Calculate a BLAKE2b hash.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string|null $key
     * @param int $outlen
     * @return string
     * @throws RangeException
     * @throws SodiumException
     * @throws TypeError
     */
    public static function generichash($message, $key = '', $outlen = 32)
    {
        // This ensures that ParagonIE_Sodium_Core32_BLAKE2b::$iv is initialized
        ParagonIE_Sodium_Core32_BLAKE2b::pseudoConstructor();

        $k = null;
        if (!empty($key)) {
            /** @var SplFixedArray $k */
            $k = ParagonIE_Sodium_Core32_BLAKE2b::stringToSplFixedArray($key);
            if ($k->count() > ParagonIE_Sodium_Core32_BLAKE2b::KEYBYTES) {
                throw new RangeException('Invalid key size');
            }
        }

        /** @var SplFixedArray $in */
        $in = ParagonIE_Sodium_Core32_BLAKE2b::stringToSplFixedArray($message);

        /** @var SplFixedArray $ctx */
        $ctx = ParagonIE_Sodium_Core32_BLAKE2b::init($k, $outlen);
        ParagonIE_Sodium_Core32_BLAKE2b::update($ctx, $in, $in->count());

        /** @var SplFixedArray $out */
        $out = new SplFixedArray($outlen);
        $out = ParagonIE_Sodium_Core32_BLAKE2b::finish($ctx, $out);

        /** @var array<int, int> */
        $outArray = $out->toArray();
        return ParagonIE_Sodium_Core32_Util::intArrayToString($outArray);
    }

    /**
     * Finalize a BLAKE2b hashing context, returning the hash.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $ctx
     * @param int $outlen
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function generichash_final($ctx, $outlen = 32)
    {
        if (!is_string($ctx)) {
            throw new TypeError('Context must be a string');
        }
        $out = new SplFixedArray($outlen);

        /** @var SplFixedArray $context */
        $context = ParagonIE_Sodium_Core32_BLAKE2b::stringToContext($ctx);

        /** @var SplFixedArray $out */
        $out = ParagonIE_Sodium_Core32_BLAKE2b::finish($context, $out);

        /** @var array<int, int> */
        $outArray = $out->toArray();
        return ParagonIE_Sodium_Core32_Util::intArrayToString($outArray);
    }

    /**
     * Initialize a hashing context for BLAKE2b.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $key
     * @param int $outputLength
     * @return string
     * @throws RangeException
     * @throws SodiumException
     * @throws TypeError
     */
    public static function generichash_init($key = '', $outputLength = 32)
    {
        // This ensures that ParagonIE_Sodium_Core32_BLAKE2b::$iv is initialized
        ParagonIE_Sodium_Core32_BLAKE2b::pseudoConstructor();

        $k = null;
        if (!empty($key)) {
            $k = ParagonIE_Sodium_Core32_BLAKE2b::stringToSplFixedArray($key);
            if ($k->count() > ParagonIE_Sodium_Core32_BLAKE2b::KEYBYTES) {
                throw new RangeException('Invalid key size');
            }
        }

        /** @var SplFixedArray $ctx */
        $ctx = ParagonIE_Sodium_Core32_BLAKE2b::init($k, $outputLength);

        return ParagonIE_Sodium_Core32_BLAKE2b::contextToString($ctx);
    }

    /**
     * Initialize a hashing context for BLAKE2b.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $key
     * @param int $outputLength
     * @param string $salt
     * @param string $personal
     * @return string
     * @throws RangeException
     * @throws SodiumException
     * @throws TypeError
     */
    public static function generichash_init_salt_personal(
        $key = '',
        $outputLength = 32,
        $salt = '',
        $personal = ''
    ) {
        // This ensures that ParagonIE_Sodium_Core32_BLAKE2b::$iv is initialized
        ParagonIE_Sodium_Core32_BLAKE2b::pseudoConstructor();

        $k = null;
        if (!empty($key)) {
            $k = ParagonIE_Sodium_Core32_BLAKE2b::stringToSplFixedArray($key);
            if ($k->count() > ParagonIE_Sodium_Core32_BLAKE2b::KEYBYTES) {
                throw new RangeException('Invalid key size');
            }
        }
        if (!empty($salt)) {
            $s = ParagonIE_Sodium_Core32_BLAKE2b::stringToSplFixedArray($salt);
        } else {
            $s = null;
        }
        if (!empty($salt)) {
            $p = ParagonIE_Sodium_Core32_BLAKE2b::stringToSplFixedArray($personal);
        } else {
            $p = null;
        }

        /** @var SplFixedArray $ctx */
        $ctx = ParagonIE_Sodium_Core32_BLAKE2b::init($k, $outputLength, $s, $p);

        return ParagonIE_Sodium_Core32_BLAKE2b::contextToString($ctx);
    }

    /**
     * Update a hashing context for BLAKE2b with $message
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $ctx
     * @param string $message
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function generichash_update($ctx, $message)
    {
        // This ensures that ParagonIE_Sodium_Core32_BLAKE2b::$iv is initialized
        ParagonIE_Sodium_Core32_BLAKE2b::pseudoConstructor();

        /** @var SplFixedArray $context */
        $context = ParagonIE_Sodium_Core32_BLAKE2b::stringToContext($ctx);

        /** @var SplFixedArray $in */
        $in = ParagonIE_Sodium_Core32_BLAKE2b::stringToSplFixedArray($message);

        ParagonIE_Sodium_Core32_BLAKE2b::update($context, $in, $in->count());

        return ParagonIE_Sodium_Core32_BLAKE2b::contextToString($context);
    }

    /**
     * Libsodium's crypto_kx().
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $my_sk
     * @param string $their_pk
     * @param string $client_pk
     * @param string $server_pk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function keyExchange($my_sk, $their_pk, $client_pk, $server_pk)
    {
        return self::generichash(
            self::scalarmult($my_sk, $their_pk) .
            $client_pk .
            $server_pk
        );
    }

    /**
     * ECDH over Curve25519
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $sKey
     * @param string $pKey
     * @return string
     *
     * @throws SodiumException
     * @throws TypeError
     */
    public static function scalarmult($sKey, $pKey)
    {
        $q = ParagonIE_Sodium_Core32_X25519::crypto_scalarmult_curve25519_ref10($sKey, $pKey);
        self::scalarmult_throw_if_zero($q);
        return $q;
    }

    /**
     * ECDH over Curve25519, using the basepoint.
     * Used to get a secret key from a public key.
     *
     * @param string $secret
     * @return string
     *
     * @throws SodiumException
     * @throws TypeError
     */
    public static function scalarmult_base($secret)
    {
        $q = ParagonIE_Sodium_Core32_X25519::crypto_scalarmult_curve25519_ref10_base($secret);
        self::scalarmult_throw_if_zero($q);
        return $q;
    }

    /**
     * This throws an Error if a zero public key was passed to the function.
     *
     * @param string $q
     * @return void
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function scalarmult_throw_if_zero($q)
    {
        $d = 0;
        for ($i = 0; $i < self::box_curve25519xsalsa20poly1305_SECRETKEYBYTES; ++$i) {
            $d |= ParagonIE_Sodium_Core32_Util::chrToInt($q[$i]);
        }

        /* branch-free variant of === 0 */
        if (-(1 & (($d - 1) >> 8))) {
            throw new SodiumException('Zero public key is not allowed');
        }
    }

    /**
     * XSalsa20-Poly1305 authenticated symmetric-key encryption.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $plaintext
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function secretbox($plaintext, $nonce, $key)
    {
        /** @var string $subkey */
        $subkey = ParagonIE_Sodium_Core32_HSalsa20::hsalsa20($nonce, $key);

        /** @var string $block0 */
        $block0 = str_repeat("\x00", 32);

        /** @var int $mlen - Length of the plaintext message */
        $mlen = ParagonIE_Sodium_Core32_Util::strlen($plaintext);
        $mlen0 = $mlen;
        if ($mlen0 > 64 - self::secretbox_xsalsa20poly1305_ZEROBYTES) {
            $mlen0 = 64 - self::secretbox_xsalsa20poly1305_ZEROBYTES;
        }
        $block0 .= ParagonIE_Sodium_Core32_Util::substr($plaintext, 0, $mlen0);

        /** @var string $block0 */
        $block0 = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor(
            $block0,
            ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8),
            $subkey
        );

        /** @var string $c */
        $c = ParagonIE_Sodium_Core32_Util::substr(
            $block0,
            self::secretbox_xsalsa20poly1305_ZEROBYTES
        );
        if ($mlen > $mlen0) {
            $c .= ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic(
                ParagonIE_Sodium_Core32_Util::substr(
                    $plaintext,
                    self::secretbox_xsalsa20poly1305_ZEROBYTES
                ),
                ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8),
                1,
                $subkey
            );
        }
        $state = new ParagonIE_Sodium_Core32_Poly1305_State(
            ParagonIE_Sodium_Core32_Util::substr(
                $block0,
                0,
                self::onetimeauth_poly1305_KEYBYTES
            )
        );
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
            ParagonIE_Sodium_Compat::memzero($subkey);
        } catch (SodiumException $ex) {
            $block0 = null;
            $subkey = null;
        }

        $state->update($c);

        /** @var string $c - MAC || ciphertext */
        $c = $state->finish() . $c;
        unset($state);

        return $c;
    }

    /**
     * Decrypt a ciphertext generated via secretbox().
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $ciphertext
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function secretbox_open($ciphertext, $nonce, $key)
    {
        /** @var string $mac */
        $mac = ParagonIE_Sodium_Core32_Util::substr(
            $ciphertext,
            0,
            self::secretbox_xsalsa20poly1305_MACBYTES
        );

        /** @var string $c */
        $c = ParagonIE_Sodium_Core32_Util::substr(
            $ciphertext,
            self::secretbox_xsalsa20poly1305_MACBYTES
        );

        /** @var int $clen */
        $clen = ParagonIE_Sodium_Core32_Util::strlen($c);

        /** @var string $subkey */
        $subkey = ParagonIE_Sodium_Core32_HSalsa20::hsalsa20($nonce, $key);

        /** @var string $block0 */
        $block0 = ParagonIE_Sodium_Core32_Salsa20::salsa20(
            64,
            ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8),
            $subkey
        );
        $verified = ParagonIE_Sodium_Core32_Poly1305::onetimeauth_verify(
            $mac,
            $c,
            ParagonIE_Sodium_Core32_Util::substr($block0, 0, 32)
        );
        if (!$verified) {
            try {
                ParagonIE_Sodium_Compat::memzero($subkey);
            } catch (SodiumException $ex) {
                $subkey = null;
            }
            throw new SodiumException('Invalid MAC');
        }

        /** @var string $m - Decrypted message */
        $m = ParagonIE_Sodium_Core32_Util::xorStrings(
            ParagonIE_Sodium_Core32_Util::substr($block0, self::secretbox_xsalsa20poly1305_ZEROBYTES),
            ParagonIE_Sodium_Core32_Util::substr($c, 0, self::secretbox_xsalsa20poly1305_ZEROBYTES)
        );
        if ($clen > self::secretbox_xsalsa20poly1305_ZEROBYTES) {
            // We had more than 1 block, so let's continue to decrypt the rest.
            $m .= ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic(
                ParagonIE_Sodium_Core32_Util::substr(
                    $c,
                    self::secretbox_xsalsa20poly1305_ZEROBYTES
                ),
                ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8),
                1,
                (string) $subkey
            );
        }
        return $m;
    }

    /**
     * XChaCha20-Poly1305 authenticated symmetric-key encryption.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $plaintext
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function secretbox_xchacha20poly1305($plaintext, $nonce, $key)
    {
        /** @var string $subkey */
        $subkey = ParagonIE_Sodium_Core32_HChaCha20::hChaCha20(
            ParagonIE_Sodium_Core32_Util::substr($nonce, 0, 16),
            $key
        );
        $nonceLast = ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8);

        /** @var string $block0 */
        $block0 = str_repeat("\x00", 32);

        /** @var int $mlen - Length of the plaintext message */
        $mlen = ParagonIE_Sodium_Core32_Util::strlen($plaintext);
        $mlen0 = $mlen;
        if ($mlen0 > 64 - self::secretbox_xchacha20poly1305_ZEROBYTES) {
            $mlen0 = 64 - self::secretbox_xchacha20poly1305_ZEROBYTES;
        }
        $block0 .= ParagonIE_Sodium_Core32_Util::substr($plaintext, 0, $mlen0);

        /** @var string $block0 */
        $block0 = ParagonIE_Sodium_Core32_ChaCha20::streamXorIc(
            $block0,
            $nonceLast,
            $subkey
        );

        /** @var string $c */
        $c = ParagonIE_Sodium_Core32_Util::substr(
            $block0,
            self::secretbox_xchacha20poly1305_ZEROBYTES
        );
        if ($mlen > $mlen0) {
            $c .= ParagonIE_Sodium_Core32_ChaCha20::streamXorIc(
                ParagonIE_Sodium_Core32_Util::substr(
                    $plaintext,
                    self::secretbox_xchacha20poly1305_ZEROBYTES
                ),
                $nonceLast,
                $subkey,
                ParagonIE_Sodium_Core32_Util::store64_le(1)
            );
        }
        $state = new ParagonIE_Sodium_Core32_Poly1305_State(
            ParagonIE_Sodium_Core32_Util::substr(
                $block0,
                0,
                self::onetimeauth_poly1305_KEYBYTES
            )
        );
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
            ParagonIE_Sodium_Compat::memzero($subkey);
        } catch (SodiumException $ex) {
            $block0 = null;
            $subkey = null;
        }

        $state->update($c);

        /** @var string $c - MAC || ciphertext */
        $c = $state->finish() . $c;
        unset($state);

        return $c;
    }

    /**
     * Decrypt a ciphertext generated via secretbox_xchacha20poly1305().
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $ciphertext
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function secretbox_xchacha20poly1305_open($ciphertext, $nonce, $key)
    {
        /** @var string $mac */
        $mac = ParagonIE_Sodium_Core32_Util::substr(
            $ciphertext,
            0,
            self::secretbox_xchacha20poly1305_MACBYTES
        );

        /** @var string $c */
        $c = ParagonIE_Sodium_Core32_Util::substr(
            $ciphertext,
            self::secretbox_xchacha20poly1305_MACBYTES
        );

        /** @var int $clen */
        $clen = ParagonIE_Sodium_Core32_Util::strlen($c);

        /** @var string $subkey */
        $subkey = ParagonIE_Sodium_Core32_HChaCha20::hchacha20($nonce, $key);

        /** @var string $block0 */
        $block0 = ParagonIE_Sodium_Core32_ChaCha20::stream(
            64,
            ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8),
            $subkey
        );
        $verified = ParagonIE_Sodium_Core32_Poly1305::onetimeauth_verify(
            $mac,
            $c,
            ParagonIE_Sodium_Core32_Util::substr($block0, 0, 32)
        );

        if (!$verified) {
            try {
                ParagonIE_Sodium_Compat::memzero($subkey);
            } catch (SodiumException $ex) {
                $subkey = null;
            }
            throw new SodiumException('Invalid MAC');
        }

        /** @var string $m - Decrypted message */
        $m = ParagonIE_Sodium_Core32_Util::xorStrings(
            ParagonIE_Sodium_Core32_Util::substr($block0, self::secretbox_xchacha20poly1305_ZEROBYTES),
            ParagonIE_Sodium_Core32_Util::substr($c, 0, self::secretbox_xchacha20poly1305_ZEROBYTES)
        );

        if ($clen > self::secretbox_xchacha20poly1305_ZEROBYTES) {
            // We had more than 1 block, so let's continue to decrypt the rest.
            $m .= ParagonIE_Sodium_Core32_ChaCha20::streamXorIc(
                ParagonIE_Sodium_Core32_Util::substr(
                    $c,
                    self::secretbox_xchacha20poly1305_ZEROBYTES
                ),
                ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8),
                (string) $subkey,
                ParagonIE_Sodium_Core32_Util::store64_le(1)
            );
        }
        return $m;
    }

    /**
     * @param string $key
     * @return array<int, string> Returns a state and a header.
     * @throws Exception
     * @throws SodiumException
     */
    public static function secretstream_xchacha20poly1305_init_push($key)
    {
        # randombytes_buf(out, crypto_secretstream_xchacha20poly1305_HEADERBYTES);
        $out = random_bytes(24);

        # crypto_core_hchacha20(state->k, out, k, NULL);
        $subkey = ParagonIE_Sodium_Core32_HChaCha20::hChaCha20($out, $key);
        $state = new ParagonIE_Sodium_Core32_SecretStream_State(
            $subkey,
            ParagonIE_Sodium_Core32_Util::substr($out, 16, 8) . str_repeat("\0", 4)
        );

        # _crypto_secretstream_xchacha20poly1305_counter_reset(state);
        $state->counterReset();

        # memcpy(STATE_INONCE(state), out + crypto_core_hchacha20_INPUTBYTES,
        #        crypto_secretstream_xchacha20poly1305_INONCEBYTES);
        # memset(state->_pad, 0, sizeof state->_pad);
        return array(
            $state->toString(),
            $out
        );
    }

    /**
     * @param string $key
     * @param string $header
     * @return string Returns a state.
     * @throws Exception
     */
    public static function secretstream_xchacha20poly1305_init_pull($key, $header)
    {
        # crypto_core_hchacha20(state->k, in, k, NULL);
        $subkey = ParagonIE_Sodium_Core32_HChaCha20::hChaCha20(
            ParagonIE_Sodium_Core32_Util::substr($header, 0, 16),
            $key
        );
        $state = new ParagonIE_Sodium_Core32_SecretStream_State(
            $subkey,
            ParagonIE_Sodium_Core32_Util::substr($header, 16)
        );
        $state->counterReset();
        # memcpy(STATE_INONCE(state), in + crypto_core_hchacha20_INPUTBYTES,
        #     crypto_secretstream_xchacha20poly1305_INONCEBYTES);
        # memset(state->_pad, 0, sizeof state->_pad);
        # return 0;
        return $state->toString();
    }

    /**
     * @param string $state
     * @param string $msg
     * @param string $aad
     * @param int $tag
     * @return string
     * @throws SodiumException
     */
    public static function secretstream_xchacha20poly1305_push(&$state, $msg, $aad = '', $tag = 0)
    {
        $st = ParagonIE_Sodium_Core32_SecretStream_State::fromString($state);
        # crypto_onetimeauth_poly1305_state poly1305_state;
        # unsigned char                     block[64U];
        # unsigned char                     slen[8U];
        # unsigned char                    *c;
        # unsigned char                    *mac;

        $msglen = ParagonIE_Sodium_Core32_Util::strlen($msg);
        $aadlen = ParagonIE_Sodium_Core32_Util::strlen($aad);

        if ((($msglen + 63) >> 6) > 0xfffffffe) {
            throw new SodiumException(
                'message cannot be larger than SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX bytes'
            );
        }

        # if (outlen_p != NULL) {
        #     *outlen_p = 0U;
        # }
        # if (mlen > crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX) {
        #     sodium_misuse();
        # }

        # crypto_stream_chacha20_ietf(block, sizeof block, state->nonce, state->k);
        # crypto_onetimeauth_poly1305_init(&poly1305_state, block);
        # sodium_memzero(block, sizeof block);
        $auth = new ParagonIE_Sodium_Core32_Poly1305_State(
            ParagonIE_Sodium_Core32_ChaCha20::ietfStream(32, $st->getCombinedNonce(), $st->getKey())
        );

        # crypto_onetimeauth_poly1305_update(&poly1305_state, ad, adlen);
        $auth->update($aad);

        # crypto_onetimeauth_poly1305_update(&poly1305_state, _pad0,
        #     (0x10 - adlen) & 0xf);
        $auth->update(str_repeat("\0", ((0x10 - $aadlen) & 0xf)));

        # memset(block, 0, sizeof block);
        # block[0] = tag;
        # crypto_stream_chacha20_ietf_xor_ic(block, block, sizeof block,
        #                                    state->nonce, 1U, state->k);
        $block = ParagonIE_Sodium_Core32_ChaCha20::ietfStreamXorIc(
            ParagonIE_Sodium_Core32_Util::intToChr($tag) . str_repeat("\0", 63),
            $st->getCombinedNonce(),
            $st->getKey(),
            ParagonIE_Sodium_Core32_Util::store64_le(1)
        );

        # crypto_onetimeauth_poly1305_update(&poly1305_state, block, sizeof block);
        $auth->update($block);

        # out[0] = block[0];
        $out = $block[0];
        # c = out + (sizeof tag);
        # crypto_stream_chacha20_ietf_xor_ic(c, m, mlen, state->nonce, 2U, state->k);
        $cipher = ParagonIE_Sodium_Core32_ChaCha20::ietfStreamXorIc(
            $msg,
            $st->getCombinedNonce(),
            $st->getKey(),
            ParagonIE_Sodium_Core32_Util::store64_le(2)
        );

        # crypto_onetimeauth_poly1305_update(&poly1305_state, c, mlen);
        $auth->update($cipher);

        $out .= $cipher;
        unset($cipher);

        # crypto_onetimeauth_poly1305_update
        # (&poly1305_state, _pad0, (0x10 - (sizeof block) + mlen) & 0xf);
        $auth->update(str_repeat("\0", ((0x10 - 64 + $msglen) & 0xf)));

        # STORE64_LE(slen, (uint64_t) adlen);
        $slen = ParagonIE_Sodium_Core32_Util::store64_le($aadlen);

        # crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen);
        $auth->update($slen);

        # STORE64_LE(slen, (sizeof block) + mlen);
        $slen = ParagonIE_Sodium_Core32_Util::store64_le(64 + $msglen);

        # crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen);
        $auth->update($slen);

        # mac = c + mlen;
        # crypto_onetimeauth_poly1305_final(&poly1305_state, mac);
        $mac = $auth->finish();
        $out .= $mac;

        # sodium_memzero(&poly1305_state, sizeof poly1305_state);
        unset($auth);


        # XOR_BUF(STATE_INONCE(state), mac,
        #     crypto_secretstream_xchacha20poly1305_INONCEBYTES);
        $st->xorNonce($mac);

        # sodium_increment(STATE_COUNTER(state),
        #     crypto_secretstream_xchacha20poly1305_COUNTERBYTES);
        $st->incrementCounter();
        // Overwrite by reference:
        $state = $st->toString();

        /** @var bool $rekey */
        $rekey = ($tag & ParagonIE_Sodium_Compat::CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_REKEY) !== 0;
        # if ((tag & crypto_secretstream_xchacha20poly1305_TAG_REKEY) != 0 ||
        #     sodium_is_zero(STATE_COUNTER(state),
        #         crypto_secretstream_xchacha20poly1305_COUNTERBYTES)) {
        #     crypto_secretstream_xchacha20poly1305_rekey(state);
        # }
        if ($rekey || $st->needsRekey()) {
            // DO REKEY
            self::secretstream_xchacha20poly1305_rekey($state);
        }
        # if (outlen_p != NULL) {
        #     *outlen_p = crypto_secretstream_xchacha20poly1305_ABYTES + mlen;
        # }
        return $out;
    }

    /**
     * @param string $state
     * @param string $cipher
     * @param string $aad
     * @return bool|array{0: string, 1: int}
     * @throws SodiumException
     */
    public static function secretstream_xchacha20poly1305_pull(&$state, $cipher, $aad = '')
    {
        $st = ParagonIE_Sodium_Core32_SecretStream_State::fromString($state);

        $cipherlen = ParagonIE_Sodium_Core32_Util::strlen($cipher);
        #     mlen = inlen - crypto_secretstream_xchacha20poly1305_ABYTES;
        $msglen = $cipherlen - ParagonIE_Sodium_Compat::CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES;
        $aadlen = ParagonIE_Sodium_Core32_Util::strlen($aad);

        #     if (mlen > crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX) {
        #         sodium_misuse();
        #     }
        if ((($msglen + 63) >> 6) > 0xfffffffe) {
            throw new SodiumException(
                'message cannot be larger than SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX bytes'
            );
        }

        #     crypto_stream_chacha20_ietf(block, sizeof block, state->nonce, state->k);
        #     crypto_onetimeauth_poly1305_init(&poly1305_state, block);
        #     sodium_memzero(block, sizeof block);
        $auth = new ParagonIE_Sodium_Core32_Poly1305_State(
            ParagonIE_Sodium_Core32_ChaCha20::ietfStream(32, $st->getCombinedNonce(), $st->getKey())
        );

        #     crypto_onetimeauth_poly1305_update(&poly1305_state, ad, adlen);
        $auth->update($aad);

        #     crypto_onetimeauth_poly1305_update(&poly1305_state, _pad0,
        #         (0x10 - adlen) & 0xf);
        $auth->update(str_repeat("\0", ((0x10 - $aadlen) & 0xf)));


        #     memset(block, 0, sizeof block);
        #     block[0] = in[0];
        #     crypto_stream_chacha20_ietf_xor_ic(block, block, sizeof block,
        #                                        state->nonce, 1U, state->k);
        $block = ParagonIE_Sodium_Core32_ChaCha20::ietfStreamXorIc(
            $cipher[0] . str_repeat("\0", 63),
            $st->getCombinedNonce(),
            $st->getKey(),
            ParagonIE_Sodium_Core32_Util::store64_le(1)
        );
        #     tag = block[0];
        #     block[0] = in[0];
        #     crypto_onetimeauth_poly1305_update(&poly1305_state, block, sizeof block);
        $tag = ParagonIE_Sodium_Core32_Util::chrToInt($block[0]);
        $block[0] = $cipher[0];
        $auth->update($block);


        #     c = in + (sizeof tag);
        #     crypto_onetimeauth_poly1305_update(&poly1305_state, c, mlen);
        $auth->update(ParagonIE_Sodium_Core32_Util::substr($cipher, 1, $msglen));

        #     crypto_onetimeauth_poly1305_update
        #     (&poly1305_state, _pad0, (0x10 - (sizeof block) + mlen) & 0xf);
        $auth->update(str_repeat("\0", ((0x10 - 64 + $msglen) & 0xf)));

        #     STORE64_LE(slen, (uint64_t) adlen);
        #     crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen);
        $slen = ParagonIE_Sodium_Core32_Util::store64_le($aadlen);
        $auth->update($slen);

        #     STORE64_LE(slen, (sizeof block) + mlen);
        #     crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen);
        $slen = ParagonIE_Sodium_Core32_Util::store64_le(64 + $msglen);
        $auth->update($slen);

        #     crypto_onetimeauth_poly1305_final(&poly1305_state, mac);
        #     sodium_memzero(&poly1305_state, sizeof poly1305_state);
        $mac = $auth->finish();

        #     stored_mac = c + mlen;
        #     if (sodium_memcmp(mac, stored_mac, sizeof mac) != 0) {
        #     sodium_memzero(mac, sizeof mac);
        #         return -1;
        #     }

        $stored = ParagonIE_Sodium_Core32_Util::substr($cipher, $msglen + 1, 16);
        if (!ParagonIE_Sodium_Core32_Util::hashEquals($mac, $stored)) {
            return false;
        }

        #     crypto_stream_chacha20_ietf_xor_ic(m, c, mlen, state->nonce, 2U, state->k);
        $out = ParagonIE_Sodium_Core32_ChaCha20::ietfStreamXorIc(
            ParagonIE_Sodium_Core32_Util::substr($cipher, 1, $msglen),
            $st->getCombinedNonce(),
            $st->getKey(),
            ParagonIE_Sodium_Core32_Util::store64_le(2)
        );

        #     XOR_BUF(STATE_INONCE(state), mac,
        #         crypto_secretstream_xchacha20poly1305_INONCEBYTES);
        $st->xorNonce($mac);

        #     sodium_increment(STATE_COUNTER(state),
        #         crypto_secretstream_xchacha20poly1305_COUNTERBYTES);
        $st->incrementCounter();

        #     if ((tag & crypto_secretstream_xchacha20poly1305_TAG_REKEY) != 0 ||
        #         sodium_is_zero(STATE_COUNTER(state),
        #             crypto_secretstream_xchacha20poly1305_COUNTERBYTES)) {
        #         crypto_secretstream_xchacha20poly1305_rekey(state);
        #     }

        // Overwrite by reference:
        $state = $st->toString();

        /** @var bool $rekey */
        $rekey = ($tag & ParagonIE_Sodium_Compat::CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_REKEY) !== 0;
        if ($rekey || $st->needsRekey()) {
            // DO REKEY
            self::secretstream_xchacha20poly1305_rekey($state);
        }
        return array($out, $tag);
    }

    /**
     * @param string $state
     * @return void
     * @throws SodiumException
     */
    public static function secretstream_xchacha20poly1305_rekey(&$state)
    {
        $st = ParagonIE_Sodium_Core32_SecretStream_State::fromString($state);
        # unsigned char new_key_and_inonce[crypto_stream_chacha20_ietf_KEYBYTES +
        # crypto_secretstream_xchacha20poly1305_INONCEBYTES];
        # size_t        i;
        # for (i = 0U; i < crypto_stream_chacha20_ietf_KEYBYTES; i++) {
        #     new_key_and_inonce[i] = state->k[i];
        # }
        $new_key_and_inonce = $st->getKey();

        # for (i = 0U; i < crypto_secretstream_xchacha20poly1305_INONCEBYTES; i++) {
        #     new_key_and_inonce[crypto_stream_chacha20_ietf_KEYBYTES + i] =
        #         STATE_INONCE(state)[i];
        # }
        $new_key_and_inonce .= ParagonIE_Sodium_Core32_Util::substR($st->getNonce(), 0, 8);

        # crypto_stream_chacha20_ietf_xor(new_key_and_inonce, new_key_and_inonce,
        #                                 sizeof new_key_and_inonce,
        #                                 state->nonce, state->k);

        $st->rekey(ParagonIE_Sodium_Core32_ChaCha20::ietfStreamXorIc(
            $new_key_and_inonce,
            $st->getCombinedNonce(),
            $st->getKey(),
            ParagonIE_Sodium_Core32_Util::store64_le(0)
        ));

        # for (i = 0U; i < crypto_stream_chacha20_ietf_KEYBYTES; i++) {
        #     state->k[i] = new_key_and_inonce[i];
        # }
        # for (i = 0U; i < crypto_secretstream_xchacha20poly1305_INONCEBYTES; i++) {
        #     STATE_INONCE(state)[i] =
        #          new_key_and_inonce[crypto_stream_chacha20_ietf_KEYBYTES + i];
        # }
        # _crypto_secretstream_xchacha20poly1305_counter_reset(state);
        $st->counterReset();

        $state = $st->toString();
    }

    /**
     * Detached Ed25519 signature.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign_detached($message, $sk)
    {
        return ParagonIE_Sodium_Core32_Ed25519::sign_detached($message, $sk);
    }

    /**
     * Attached Ed25519 signature. (Returns a signed message.)
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign($message, $sk)
    {
        return ParagonIE_Sodium_Core32_Ed25519::sign($message, $sk);
    }

    /**
     * Opens a signed message. If valid, returns the message.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $signedMessage
     * @param string $pk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign_open($signedMessage, $pk)
    {
        return ParagonIE_Sodium_Core32_Ed25519::sign_open($signedMessage, $pk);
    }

    /**
     * Verify a detached signature of a given message and public key.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $signature
     * @param string $message
     * @param string $pk
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign_verify_detached($signature, $message, $pk)
    {
        return ParagonIE_Sodium_Core32_Ed25519::verify_detached($signature, $message, $pk);
    }
}
Compat.php000064400000440246152331075510006514 0ustar00<?php

/**
 * Libsodium compatibility layer
 *
 * This is the only class you should be interfacing with, as a user of
 * sodium_compat.
 *
 * If the PHP extension for libsodium is installed, it will always use that
 * instead of our implementations. You get better performance and stronger
 * guarantees against side-channels that way.
 *
 * However, if your users don't have the PHP extension installed, we offer a
 * compatible interface here. It will give you the correct results as if the
 * PHP extension was installed. It won't be as fast, of course.
 *
 * CAUTION * CAUTION * CAUTION * CAUTION * CAUTION * CAUTION * CAUTION * CAUTION *
 *                                                                               *
 *     Until audited, this is probably not safe to use! DANGER WILL ROBINSON     *
 *                                                                               *
 * CAUTION * CAUTION * CAUTION * CAUTION * CAUTION * CAUTION * CAUTION * CAUTION *
 */

if (class_exists('ParagonIE_Sodium_Compat', false)) {
    return;
}

class ParagonIE_Sodium_Compat
{
    /**
     * This parameter prevents the use of the PECL extension.
     * It should only be used for unit testing.
     *
     * @var bool
     */
    public static $disableFallbackForUnitTests = false;

    /**
     * Use fast multiplication rather than our constant-time multiplication
     * implementation. Can be enabled at runtime. Only enable this if you
     * are absolutely certain that there is no timing leak on your platform.
     *
     * @var bool
     */
    public static $fastMult = false;

    const LIBRARY_MAJOR_VERSION = 9;
    const LIBRARY_MINOR_VERSION = 1;
    const LIBRARY_VERSION_MAJOR = 9;
    const LIBRARY_VERSION_MINOR = 1;
    const VERSION_STRING = 'polyfill-1.0.8';

    // From libsodium
    const BASE64_VARIANT_ORIGINAL = 1;
    const BASE64_VARIANT_ORIGINAL_NO_PADDING = 3;
    const BASE64_VARIANT_URLSAFE = 5;
    const BASE64_VARIANT_URLSAFE_NO_PADDING = 7;
    const CRYPTO_AEAD_AES256GCM_KEYBYTES = 32;
    const CRYPTO_AEAD_AES256GCM_NSECBYTES = 0;
    const CRYPTO_AEAD_AES256GCM_NPUBBYTES = 12;
    const CRYPTO_AEAD_AES256GCM_ABYTES = 16;
    const CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES = 32;
    const CRYPTO_AEAD_CHACHA20POLY1305_NSECBYTES = 0;
    const CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES = 8;
    const CRYPTO_AEAD_CHACHA20POLY1305_ABYTES = 16;
    const CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES = 32;
    const CRYPTO_AEAD_CHACHA20POLY1305_IETF_NSECBYTES = 0;
    const CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES = 12;
    const CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES = 16;
    const CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES = 32;
    const CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NSECBYTES = 0;
    const CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES = 24;
    const CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES = 16;
    const CRYPTO_AUTH_BYTES = 32;
    const CRYPTO_AUTH_KEYBYTES = 32;
    const CRYPTO_BOX_SEALBYTES = 16;
    const CRYPTO_BOX_SECRETKEYBYTES = 32;
    const CRYPTO_BOX_PUBLICKEYBYTES = 32;
    const CRYPTO_BOX_KEYPAIRBYTES = 64;
    const CRYPTO_BOX_MACBYTES = 16;
    const CRYPTO_BOX_NONCEBYTES = 24;
    const CRYPTO_BOX_SEEDBYTES = 32;
    const CRYPTO_CORE_RISTRETTO255_BYTES = 32;
    const CRYPTO_CORE_RISTRETTO255_SCALARBYTES = 32;
    const CRYPTO_CORE_RISTRETTO255_HASHBYTES = 64;
    const CRYPTO_CORE_RISTRETTO255_NONREDUCEDSCALARBYTES = 64;
    const CRYPTO_KDF_BYTES_MIN = 16;
    const CRYPTO_KDF_BYTES_MAX = 64;
    const CRYPTO_KDF_CONTEXTBYTES = 8;
    const CRYPTO_KDF_KEYBYTES = 32;
    const CRYPTO_KX_BYTES = 32;
    const CRYPTO_KX_PRIMITIVE = 'x25519blake2b';
    const CRYPTO_KX_SEEDBYTES = 32;
    const CRYPTO_KX_KEYPAIRBYTES = 64;
    const CRYPTO_KX_PUBLICKEYBYTES = 32;
    const CRYPTO_KX_SECRETKEYBYTES = 32;
    const CRYPTO_KX_SESSIONKEYBYTES = 32;
    const CRYPTO_GENERICHASH_BYTES = 32;
    const CRYPTO_GENERICHASH_BYTES_MIN = 16;
    const CRYPTO_GENERICHASH_BYTES_MAX = 64;
    const CRYPTO_GENERICHASH_KEYBYTES = 32;
    const CRYPTO_GENERICHASH_KEYBYTES_MIN = 16;
    const CRYPTO_GENERICHASH_KEYBYTES_MAX = 64;
    const CRYPTO_PWHASH_SALTBYTES = 16;
    const CRYPTO_PWHASH_STRPREFIX = '$argon2id$';
    const CRYPTO_PWHASH_ALG_ARGON2I13 = 1;
    const CRYPTO_PWHASH_ALG_ARGON2ID13 = 2;
    const CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE = 33554432;
    const CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE = 4;
    const CRYPTO_PWHASH_MEMLIMIT_MODERATE = 134217728;
    const CRYPTO_PWHASH_OPSLIMIT_MODERATE = 6;
    const CRYPTO_PWHASH_MEMLIMIT_SENSITIVE = 536870912;
    const CRYPTO_PWHASH_OPSLIMIT_SENSITIVE = 8;
    const CRYPTO_PWHASH_SCRYPTSALSA208SHA256_SALTBYTES = 32;
    const CRYPTO_PWHASH_SCRYPTSALSA208SHA256_STRPREFIX = '$7$';
    const CRYPTO_PWHASH_SCRYPTSALSA208SHA256_OPSLIMIT_INTERACTIVE = 534288;
    const CRYPTO_PWHASH_SCRYPTSALSA208SHA256_MEMLIMIT_INTERACTIVE = 16777216;
    const CRYPTO_PWHASH_SCRYPTSALSA208SHA256_OPSLIMIT_SENSITIVE = 33554432;
    const CRYPTO_PWHASH_SCRYPTSALSA208SHA256_MEMLIMIT_SENSITIVE = 1073741824;
    const CRYPTO_SCALARMULT_BYTES = 32;
    const CRYPTO_SCALARMULT_SCALARBYTES = 32;
    const CRYPTO_SCALARMULT_RISTRETTO255_BYTES = 32;
    const CRYPTO_SCALARMULT_RISTRETTO255_SCALARBYTES = 32;
    const CRYPTO_SHORTHASH_BYTES = 8;
    const CRYPTO_SHORTHASH_KEYBYTES = 16;
    const CRYPTO_SECRETBOX_KEYBYTES = 32;
    const CRYPTO_SECRETBOX_MACBYTES = 16;
    const CRYPTO_SECRETBOX_NONCEBYTES = 24;
    const CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES = 17;
    const CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES = 24;
    const CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_KEYBYTES = 32;
    const CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_PUSH = 0;
    const CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_PULL = 1;
    const CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_REKEY = 2;
    const CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL = 3;
    const CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX = 0x3fffffff80;
    const CRYPTO_SIGN_BYTES = 64;
    const CRYPTO_SIGN_SEEDBYTES = 32;
    const CRYPTO_SIGN_PUBLICKEYBYTES = 32;
    const CRYPTO_SIGN_SECRETKEYBYTES = 64;
    const CRYPTO_SIGN_KEYPAIRBYTES = 96;
    const CRYPTO_STREAM_KEYBYTES = 32;
    const CRYPTO_STREAM_NONCEBYTES = 24;
    const CRYPTO_STREAM_XCHACHA20_KEYBYTES = 32;
    const CRYPTO_STREAM_XCHACHA20_NONCEBYTES = 24;

    /**
     * Add two numbers (little-endian unsigned), storing the value in the first
     * parameter.
     *
     * This mutates $val.
     *
     * @param string $val
     * @param string $addv
     * @return void
     * @throws SodiumException
     */
    public static function add(&$val, $addv)
    {
        $val_len = ParagonIE_Sodium_Core_Util::strlen($val);
        $addv_len = ParagonIE_Sodium_Core_Util::strlen($addv);
        if ($val_len !== $addv_len) {
            throw new SodiumException('values must have the same length');
        }
        $A = ParagonIE_Sodium_Core_Util::stringToIntArray($val);
        $B = ParagonIE_Sodium_Core_Util::stringToIntArray($addv);

        $c = 0;
        for ($i = 0; $i < $val_len; $i++) {
            $c += ($A[$i] + $B[$i]);
            $A[$i] = ($c & 0xff);
            $c >>= 8;
        }
        $val = ParagonIE_Sodium_Core_Util::intArrayToString($A);
    }

    /**
     * @param string $encoded
     * @param int $variant
     * @param string $ignore
     * @return string
     * @throws SodiumException
     */
    public static function base642bin($encoded, $variant, $ignore = '')
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($encoded, 'string', 1);

        /** @var string $encoded */
        $encoded = (string) $encoded;
        if (ParagonIE_Sodium_Core_Util::strlen($encoded) === 0) {
            return '';
        }

        // Just strip before decoding
        if (!empty($ignore)) {
            $encoded = str_replace($ignore, '', $encoded);
        }

        try {
            switch ($variant) {
                case self::BASE64_VARIANT_ORIGINAL:
                    return ParagonIE_Sodium_Core_Base64_Original::decode($encoded, true);
                case self::BASE64_VARIANT_ORIGINAL_NO_PADDING:
                    return ParagonIE_Sodium_Core_Base64_Original::decode($encoded, false);
                case self::BASE64_VARIANT_URLSAFE:
                    return ParagonIE_Sodium_Core_Base64_UrlSafe::decode($encoded, true);
                case self::BASE64_VARIANT_URLSAFE_NO_PADDING:
                    return ParagonIE_Sodium_Core_Base64_UrlSafe::decode($encoded, false);
                default:
                    throw new SodiumException('invalid base64 variant identifier');
            }
        } catch (Exception $ex) {
            if ($ex instanceof SodiumException) {
                throw $ex;
            }
            throw new SodiumException('invalid base64 string');
        }
    }

    /**
     * @param string $decoded
     * @param int $variant
     * @return string
     * @throws SodiumException
     */
    public static function bin2base64($decoded, $variant)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($decoded, 'string', 1);
        /** @var string $decoded */
        $decoded = (string) $decoded;
        if (ParagonIE_Sodium_Core_Util::strlen($decoded) === 0) {
            return '';
        }

        switch ($variant) {
            case self::BASE64_VARIANT_ORIGINAL:
                return ParagonIE_Sodium_Core_Base64_Original::encode($decoded);
            case self::BASE64_VARIANT_ORIGINAL_NO_PADDING:
                return ParagonIE_Sodium_Core_Base64_Original::encodeUnpadded($decoded);
            case self::BASE64_VARIANT_URLSAFE:
                return ParagonIE_Sodium_Core_Base64_UrlSafe::encode($decoded);
            case self::BASE64_VARIANT_URLSAFE_NO_PADDING:
                return ParagonIE_Sodium_Core_Base64_UrlSafe::encodeUnpadded($decoded);
            default:
                throw new SodiumException('invalid base64 variant identifier');
        }
    }

    /**
     * Cache-timing-safe implementation of bin2hex().
     *
     * @param string $string A string (probably raw binary)
     * @return string        A hexadecimal-encoded string
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function bin2hex($string)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($string, 'string', 1);

        if (self::useNewSodiumAPI()) {
            return (string) sodium_bin2hex($string);
        }
        if (self::use_fallback('bin2hex')) {
            return (string) call_user_func('\\Sodium\\bin2hex', $string);
        }
        return ParagonIE_Sodium_Core_Util::bin2hex($string);
    }

    /**
     * Compare two strings, in constant-time.
     * Compared to memcmp(), compare() is more useful for sorting.
     *
     * @param string $left  The left operand; must be a string
     * @param string $right The right operand; must be a string
     * @return int          If < 0 if the left operand is less than the right
     *                      If = 0 if both strings are equal
     *                      If > 0 if the right operand is less than the left
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function compare($left, $right)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($left, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($right, 'string', 2);

        if (self::useNewSodiumAPI()) {
            return (int) sodium_compare($left, $right);
        }
        if (self::use_fallback('compare')) {
            return (int) call_user_func('\\Sodium\\compare', $left, $right);
        }
        return ParagonIE_Sodium_Core_Util::compare($left, $right);
    }

    /**
     * Is AES-256-GCM even available to use?
     *
     * @return bool
     * @psalm-suppress UndefinedFunction
     * @psalm-suppress MixedInferredReturnType
     * @psalm-suppress MixedReturnStatement
     */
    public static function crypto_aead_aes256gcm_is_available()
    {
        if (self::useNewSodiumAPI()) {
            return sodium_crypto_aead_aes256gcm_is_available();
        }
        if (self::use_fallback('crypto_aead_aes256gcm_is_available')) {
            return call_user_func('\\Sodium\\crypto_aead_aes256gcm_is_available');
        }
        if (PHP_VERSION_ID < 70100) {
            // OpenSSL doesn't support AEAD before 7.1.0
            return false;
        }
        if (!is_callable('openssl_encrypt') || !is_callable('openssl_decrypt')) {
            // OpenSSL isn't installed
            return false;
        }
        return (bool) in_array('aes-256-gcm', openssl_get_cipher_methods());
    }

    /**
     * Authenticated Encryption with Associated Data: Decryption
     *
     * Algorithm:
     *     AES-256-GCM
     *
     * This mode uses a 64-bit random nonce with a 64-bit counter.
     * IETF mode uses a 96-bit random nonce with a 32-bit counter.
     *
     * @param string $ciphertext Encrypted message (with Poly1305 MAC appended)
     * @param string $assocData  Authenticated Associated Data (unencrypted)
     * @param string $nonce      Number to be used only Once; must be 8 bytes
     * @param string $key        Encryption key
     *
     * @return string|bool       The original plaintext message
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedInferredReturnType
     * @psalm-suppress MixedReturnStatement
     */
    public static function crypto_aead_aes256gcm_decrypt(
        $ciphertext = '',
        $assocData = '',
        $nonce = '',
        $key = ''
    ) {
        if (!self::crypto_aead_aes256gcm_is_available()) {
            throw new SodiumException('AES-256-GCM is not available');
        }
        ParagonIE_Sodium_Core_Util::declareScalarType($ciphertext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($assocData, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_AEAD_AES256GCM_NPUBBYTES) {
            throw new SodiumException('Nonce must be CRYPTO_AEAD_AES256GCM_NPUBBYTES long');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AEAD_AES256GCM_KEYBYTES) {
            throw new SodiumException('Key must be CRYPTO_AEAD_AES256GCM_KEYBYTES long');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($ciphertext) < self::CRYPTO_AEAD_AES256GCM_ABYTES) {
            throw new SodiumException('Message must be at least CRYPTO_AEAD_AES256GCM_ABYTES long');
        }
        if (!is_callable('openssl_decrypt')) {
            throw new SodiumException('The OpenSSL extension is not installed, or openssl_decrypt() is not available');
        }

        /** @var string $ctext */
        $ctext = ParagonIE_Sodium_Core_Util::substr($ciphertext, 0, -self::CRYPTO_AEAD_AES256GCM_ABYTES);
        /** @var string $authTag */
        $authTag = ParagonIE_Sodium_Core_Util::substr($ciphertext, -self::CRYPTO_AEAD_AES256GCM_ABYTES, 16);
        return openssl_decrypt(
            $ctext,
            'aes-256-gcm',
            $key,
            OPENSSL_RAW_DATA,
            $nonce,
            $authTag,
            $assocData
        );
    }

    /**
     * Authenticated Encryption with Associated Data: Encryption
     *
     * Algorithm:
     *     AES-256-GCM
     *
     * @param string $plaintext Message to be encrypted
     * @param string $assocData Authenticated Associated Data (unencrypted)
     * @param string $nonce     Number to be used only Once; must be 8 bytes
     * @param string $key       Encryption key
     *
     * @return string           Ciphertext with a 16-byte GCM message
     *                          authentication code appended
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_aead_aes256gcm_encrypt(
        $plaintext = '',
        $assocData = '',
        $nonce = '',
        $key = ''
    ) {
        if (!self::crypto_aead_aes256gcm_is_available()) {
            throw new SodiumException('AES-256-GCM is not available');
        }
        ParagonIE_Sodium_Core_Util::declareScalarType($plaintext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($assocData, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_AEAD_AES256GCM_NPUBBYTES) {
            throw new SodiumException('Nonce must be CRYPTO_AEAD_AES256GCM_NPUBBYTES long');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AEAD_AES256GCM_KEYBYTES) {
            throw new SodiumException('Key must be CRYPTO_AEAD_AES256GCM_KEYBYTES long');
        }

        if (!is_callable('openssl_encrypt')) {
            throw new SodiumException('The OpenSSL extension is not installed, or openssl_encrypt() is not available');
        }

        $authTag = '';
        $ciphertext = openssl_encrypt(
            $plaintext,
            'aes-256-gcm',
            $key,
            OPENSSL_RAW_DATA,
            $nonce,
            $authTag,
            $assocData
        );
        return $ciphertext . $authTag;
    }

    /**
     * Return a secure random key for use with the AES-256-GCM
     * symmetric AEAD interface.
     *
     * @return string
     * @throws Exception
     * @throws Error
     */
    public static function crypto_aead_aes256gcm_keygen()
    {
        return random_bytes(self::CRYPTO_AEAD_AES256GCM_KEYBYTES);
    }

    /**
     * Authenticated Encryption with Associated Data: Decryption
     *
     * Algorithm:
     *     ChaCha20-Poly1305
     *
     * This mode uses a 64-bit random nonce with a 64-bit counter.
     * IETF mode uses a 96-bit random nonce with a 32-bit counter.
     *
     * @param string $ciphertext Encrypted message (with Poly1305 MAC appended)
     * @param string $assocData  Authenticated Associated Data (unencrypted)
     * @param string $nonce      Number to be used only Once; must be 8 bytes
     * @param string $key        Encryption key
     *
     * @return string            The original plaintext message
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedInferredReturnType
     * @psalm-suppress MixedReturnStatement
     */
    public static function crypto_aead_chacha20poly1305_decrypt(
        $ciphertext = '',
        $assocData = '',
        $nonce = '',
        $key = ''
    ) {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($ciphertext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($assocData, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES) {
            throw new SodiumException('Nonce must be CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES long');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES) {
            throw new SodiumException('Key must be CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES long');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($ciphertext) < self::CRYPTO_AEAD_CHACHA20POLY1305_ABYTES) {
            throw new SodiumException('Message must be at least CRYPTO_AEAD_CHACHA20POLY1305_ABYTES long');
        }

        if (self::useNewSodiumAPI()) {
            /**
             * @psalm-suppress InvalidReturnStatement
             * @psalm-suppress FalsableReturnStatement
             */
            return sodium_crypto_aead_chacha20poly1305_decrypt(
                $ciphertext,
                $assocData,
                $nonce,
                $key
            );
        }
        if (self::use_fallback('crypto_aead_chacha20poly1305_decrypt')) {
            return call_user_func(
                '\\Sodium\\crypto_aead_chacha20poly1305_decrypt',
                $ciphertext,
                $assocData,
                $nonce,
                $key
            );
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::aead_chacha20poly1305_decrypt(
                $ciphertext,
                $assocData,
                $nonce,
                $key
            );
        }
        return ParagonIE_Sodium_Crypto::aead_chacha20poly1305_decrypt(
            $ciphertext,
            $assocData,
            $nonce,
            $key
        );
    }

    /**
     * Authenticated Encryption with Associated Data
     *
     * Algorithm:
     *     ChaCha20-Poly1305
     *
     * This mode uses a 64-bit random nonce with a 64-bit counter.
     * IETF mode uses a 96-bit random nonce with a 32-bit counter.
     *
     * @param string $plaintext Message to be encrypted
     * @param string $assocData Authenticated Associated Data (unencrypted)
     * @param string $nonce     Number to be used only Once; must be 8 bytes
     * @param string $key       Encryption key
     *
     * @return string           Ciphertext with a 16-byte Poly1305 message
     *                          authentication code appended
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_aead_chacha20poly1305_encrypt(
        $plaintext = '',
        $assocData = '',
        $nonce = '',
        $key = ''
    ) {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($plaintext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($assocData, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES) {
            throw new SodiumException('Nonce must be CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES long');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES) {
            throw new SodiumException('Key must be CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES long');
        }

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_aead_chacha20poly1305_encrypt(
                $plaintext,
                $assocData,
                $nonce,
                $key
            );
        }
        if (self::use_fallback('crypto_aead_chacha20poly1305_encrypt')) {
            return (string) call_user_func(
                '\\Sodium\\crypto_aead_chacha20poly1305_encrypt',
                $plaintext,
                $assocData,
                $nonce,
                $key
            );
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::aead_chacha20poly1305_encrypt(
                $plaintext,
                $assocData,
                $nonce,
                $key
            );
        }
        return ParagonIE_Sodium_Crypto::aead_chacha20poly1305_encrypt(
            $plaintext,
            $assocData,
            $nonce,
            $key
        );
    }

    /**
     * Authenticated Encryption with Associated Data: Decryption
     *
     * Algorithm:
     *     ChaCha20-Poly1305
     *
     * IETF mode uses a 96-bit random nonce with a 32-bit counter.
     * Regular mode uses a 64-bit random nonce with a 64-bit counter.
     *
     * @param string $ciphertext Encrypted message (with Poly1305 MAC appended)
     * @param string $assocData  Authenticated Associated Data (unencrypted)
     * @param string $nonce      Number to be used only Once; must be 12 bytes
     * @param string $key        Encryption key
     *
     * @return string            The original plaintext message
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedInferredReturnType
     * @psalm-suppress MixedReturnStatement
     */
    public static function crypto_aead_chacha20poly1305_ietf_decrypt(
        $ciphertext = '',
        $assocData = '',
        $nonce = '',
        $key = ''
    ) {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($ciphertext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($assocData, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES) {
            throw new SodiumException('Nonce must be CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES long');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES) {
            throw new SodiumException('Key must be CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES long');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($ciphertext) < self::CRYPTO_AEAD_CHACHA20POLY1305_ABYTES) {
            throw new SodiumException('Message must be at least CRYPTO_AEAD_CHACHA20POLY1305_ABYTES long');
        }

        if (self::useNewSodiumAPI()) {
            /**
             * @psalm-suppress InvalidReturnStatement
             * @psalm-suppress FalsableReturnStatement
             */
            return sodium_crypto_aead_chacha20poly1305_ietf_decrypt(
                $ciphertext,
                $assocData,
                $nonce,
                $key
            );
        }
        if (self::use_fallback('crypto_aead_chacha20poly1305_ietf_decrypt')) {
            return call_user_func(
                '\\Sodium\\crypto_aead_chacha20poly1305_ietf_decrypt',
                $ciphertext,
                $assocData,
                $nonce,
                $key
            );
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::aead_chacha20poly1305_ietf_decrypt(
                $ciphertext,
                $assocData,
                $nonce,
                $key
            );
        }
        return ParagonIE_Sodium_Crypto::aead_chacha20poly1305_ietf_decrypt(
            $ciphertext,
            $assocData,
            $nonce,
            $key
        );
    }

    /**
     * Return a secure random key for use with the ChaCha20-Poly1305
     * symmetric AEAD interface.
     *
     * @return string
     * @throws Exception
     * @throws Error
     */
    public static function crypto_aead_chacha20poly1305_keygen()
    {
        return random_bytes(self::CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES);
    }

    /**
     * Authenticated Encryption with Associated Data
     *
     * Algorithm:
     *     ChaCha20-Poly1305
     *
     * IETF mode uses a 96-bit random nonce with a 32-bit counter.
     * Regular mode uses a 64-bit random nonce with a 64-bit counter.
     *
     * @param string $plaintext Message to be encrypted
     * @param string $assocData Authenticated Associated Data (unencrypted)
     * @param string $nonce Number to be used only Once; must be 8 bytes
     * @param string $key Encryption key
     *
     * @return string           Ciphertext with a 16-byte Poly1305 message
     *                          authentication code appended
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_aead_chacha20poly1305_ietf_encrypt(
        $plaintext = '',
        $assocData = '',
        $nonce = '',
        $key = ''
    ) {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($plaintext, 'string', 1);
        if (!is_null($assocData)) {
            ParagonIE_Sodium_Core_Util::declareScalarType($assocData, 'string', 2);
        }
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES) {
            throw new SodiumException('Nonce must be CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES long');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES) {
            throw new SodiumException('Key must be CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES long');
        }

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_aead_chacha20poly1305_ietf_encrypt(
                $plaintext,
                $assocData,
                $nonce,
                $key
            );
        }
        if (self::use_fallback('crypto_aead_chacha20poly1305_ietf_encrypt')) {
            return (string) call_user_func(
                '\\Sodium\\crypto_aead_chacha20poly1305_ietf_encrypt',
                $plaintext,
                $assocData,
                $nonce,
                $key
            );
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::aead_chacha20poly1305_ietf_encrypt(
                $plaintext,
                $assocData,
                $nonce,
                $key
            );
        }
        return ParagonIE_Sodium_Crypto::aead_chacha20poly1305_ietf_encrypt(
            $plaintext,
            $assocData,
            $nonce,
            $key
        );
    }

    /**
     * Return a secure random key for use with the ChaCha20-Poly1305
     * symmetric AEAD interface. (IETF version)
     *
     * @return string
     * @throws Exception
     * @throws Error
     */
    public static function crypto_aead_chacha20poly1305_ietf_keygen()
    {
        return random_bytes(self::CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES);
    }

    /**
     * Authenticated Encryption with Associated Data: Decryption
     *
     * Algorithm:
     *     XChaCha20-Poly1305
     *
     * This mode uses a 64-bit random nonce with a 64-bit counter.
     * IETF mode uses a 96-bit random nonce with a 32-bit counter.
     *
     * @param string $ciphertext   Encrypted message (with Poly1305 MAC appended)
     * @param string $assocData    Authenticated Associated Data (unencrypted)
     * @param string $nonce        Number to be used only Once; must be 8 bytes
     * @param string $key          Encryption key
     * @param bool   $dontFallback Don't fallback to ext/sodium
     *
     * @return string|bool         The original plaintext message
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_aead_xchacha20poly1305_ietf_decrypt(
        $ciphertext = '',
        $assocData = '',
        $nonce = '',
        $key = '',
        $dontFallback = false
    ) {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($ciphertext, 'string', 1);
        if (!is_null($assocData)) {
            ParagonIE_Sodium_Core_Util::declareScalarType($assocData, 'string', 2);
        } else {
            $assocData = '';
        }
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES) {
            throw new SodiumException('Nonce must be CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES long');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES) {
            throw new SodiumException('Key must be CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES long');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($ciphertext) < self::CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES) {
            throw new SodiumException('Message must be at least CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES long');
        }
        if (self::useNewSodiumAPI() && !$dontFallback) {
            if (is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) {
                return sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
                    $ciphertext,
                    $assocData,
                    $nonce,
                    $key
                );
            }
        }

        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::aead_xchacha20poly1305_ietf_decrypt(
                $ciphertext,
                $assocData,
                $nonce,
                $key
            );
        }
        return ParagonIE_Sodium_Crypto::aead_xchacha20poly1305_ietf_decrypt(
            $ciphertext,
            $assocData,
            $nonce,
            $key
        );
    }

    /**
     * Authenticated Encryption with Associated Data
     *
     * Algorithm:
     *     XChaCha20-Poly1305
     *
     * This mode uses a 64-bit random nonce with a 64-bit counter.
     * IETF mode uses a 96-bit random nonce with a 32-bit counter.
     *
     * @param string $plaintext    Message to be encrypted
     * @param string $assocData    Authenticated Associated Data (unencrypted)
     * @param string $nonce        Number to be used only Once; must be 8 bytes
     * @param string $key          Encryption key
     * @param bool   $dontFallback Don't fallback to ext/sodium
     *
     * @return string           Ciphertext with a 16-byte Poly1305 message
     *                          authentication code appended
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_aead_xchacha20poly1305_ietf_encrypt(
        $plaintext = '',
        $assocData = '',
        $nonce = '',
        $key = '',
        $dontFallback = false
    ) {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($plaintext, 'string', 1);
        if (!is_null($assocData)) {
            ParagonIE_Sodium_Core_Util::declareScalarType($assocData, 'string', 2);
        } else {
            $assocData = '';
        }
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES) {
            throw new SodiumException('Nonce must be CRYPTO_AEAD_XCHACHA20POLY1305_NPUBBYTES long');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES) {
            throw new SodiumException('Key must be CRYPTO_AEAD_XCHACHA20POLY1305_KEYBYTES long');
        }
        if (self::useNewSodiumAPI() && !$dontFallback) {
            if (is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) {
                return sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(
                    $plaintext,
                    $assocData,
                    $nonce,
                    $key
                );
            }
        }

        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::aead_xchacha20poly1305_ietf_encrypt(
                $plaintext,
                $assocData,
                $nonce,
                $key
            );
        }
        return ParagonIE_Sodium_Crypto::aead_xchacha20poly1305_ietf_encrypt(
            $plaintext,
            $assocData,
            $nonce,
            $key
        );
    }

    /**
     * Return a secure random key for use with the XChaCha20-Poly1305
     * symmetric AEAD interface.
     *
     * @return string
     * @throws Exception
     * @throws Error
     */
    public static function crypto_aead_xchacha20poly1305_ietf_keygen()
    {
        return random_bytes(self::CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES);
    }

    /**
     * Authenticate a message. Uses symmetric-key cryptography.
     *
     * Algorithm:
     *     HMAC-SHA512-256. Which is HMAC-SHA-512 truncated to 256 bits.
     *     Not to be confused with HMAC-SHA-512/256 which would use the
     *     SHA-512/256 hash function (uses different initial parameters
     *     but still truncates to 256 bits to sidestep length-extension
     *     attacks).
     *
     * @param string $message Message to be authenticated
     * @param string $key Symmetric authentication key
     * @return string         Message authentication code
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_auth($message, $key)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($message, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 2);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AUTH_KEYBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_AUTH_KEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_auth($message, $key);
        }
        if (self::use_fallback('crypto_auth')) {
            return (string) call_user_func('\\Sodium\\crypto_auth', $message, $key);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::auth($message, $key);
        }
        return ParagonIE_Sodium_Crypto::auth($message, $key);
    }

    /**
     * @return string
     * @throws Exception
     * @throws Error
     */
    public static function crypto_auth_keygen()
    {
        return random_bytes(self::CRYPTO_AUTH_KEYBYTES);
    }

    /**
     * Verify the MAC of a message previously authenticated with crypto_auth.
     *
     * @param string $mac Message authentication code
     * @param string $message Message whose authenticity you are attempting to
     *                        verify (with a given MAC and key)
     * @param string $key Symmetric authentication key
     * @return bool           TRUE if authenticated, FALSE otherwise
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_auth_verify($mac, $message, $key)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($mac, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($message, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 3);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($mac) !== self::CRYPTO_AUTH_BYTES) {
            throw new SodiumException('Argument 1 must be CRYPTO_AUTH_BYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AUTH_KEYBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_AUTH_KEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return (bool) sodium_crypto_auth_verify($mac, $message, $key);
        }
        if (self::use_fallback('crypto_auth_verify')) {
            return (bool) call_user_func('\\Sodium\\crypto_auth_verify', $mac, $message, $key);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::auth_verify($mac, $message, $key);
        }
        return ParagonIE_Sodium_Crypto::auth_verify($mac, $message, $key);
    }

    /**
     * Authenticated asymmetric-key encryption. Both the sender and recipient
     * may decrypt messages.
     *
     * Algorithm: X25519-XSalsa20-Poly1305.
     *     X25519: Elliptic-Curve Diffie Hellman over Curve25519.
     *     XSalsa20: Extended-nonce variant of salsa20.
     *     Poyl1305: Polynomial MAC for one-time message authentication.
     *
     * @param string $plaintext The message to be encrypted
     * @param string $nonce A Number to only be used Once; must be 24 bytes
     * @param string $keypair Your secret key and your recipient's public key
     * @return string           Ciphertext with 16-byte Poly1305 MAC
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_box($plaintext, $nonce, $keypair)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($plaintext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($keypair, 'string', 3);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_BOX_NONCEBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_BOX_NONCEBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($keypair) !== self::CRYPTO_BOX_KEYPAIRBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_BOX_KEYPAIRBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_box($plaintext, $nonce, $keypair);
        }
        if (self::use_fallback('crypto_box')) {
            return (string) call_user_func('\\Sodium\\crypto_box', $plaintext, $nonce, $keypair);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::box($plaintext, $nonce, $keypair);
        }
        return ParagonIE_Sodium_Crypto::box($plaintext, $nonce, $keypair);
    }

    /**
     * Anonymous public-key encryption. Only the recipient may decrypt messages.
     *
     * Algorithm: X25519-XSalsa20-Poly1305, as with crypto_box.
     *     The sender's X25519 keypair is ephemeral.
     *     Nonce is generated from the BLAKE2b hash of both public keys.
     *
     * This provides ciphertext integrity.
     *
     * @param string $plaintext Message to be sealed
     * @param string $publicKey Your recipient's public key
     * @return string           Sealed message that only your recipient can
     *                          decrypt
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_box_seal($plaintext, $publicKey)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($plaintext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($publicKey, 'string', 2);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($publicKey) !== self::CRYPTO_BOX_PUBLICKEYBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_BOX_PUBLICKEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_box_seal($plaintext, $publicKey);
        }
        if (self::use_fallback('crypto_box_seal')) {
            return (string) call_user_func('\\Sodium\\crypto_box_seal', $plaintext, $publicKey);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::box_seal($plaintext, $publicKey);
        }
        return ParagonIE_Sodium_Crypto::box_seal($plaintext, $publicKey);
    }

    /**
     * Opens a message encrypted with crypto_box_seal(). Requires
     * the recipient's keypair (sk || pk) to decrypt successfully.
     *
     * This validates ciphertext integrity.
     *
     * @param string $ciphertext Sealed message to be opened
     * @param string $keypair    Your crypto_box keypair
     * @return string            The original plaintext message
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedInferredReturnType
     * @psalm-suppress MixedReturnStatement
     */
    public static function crypto_box_seal_open($ciphertext, $keypair)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($ciphertext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($keypair, 'string', 2);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($keypair) !== self::CRYPTO_BOX_KEYPAIRBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_BOX_KEYPAIRBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            /**
             * @psalm-suppress InvalidReturnStatement
             * @psalm-suppress FalsableReturnStatement
             */
            return sodium_crypto_box_seal_open($ciphertext, $keypair);
        }
        if (self::use_fallback('crypto_box_seal_open')) {
            return call_user_func('\\Sodium\\crypto_box_seal_open', $ciphertext, $keypair);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::box_seal_open($ciphertext, $keypair);
        }
        return ParagonIE_Sodium_Crypto::box_seal_open($ciphertext, $keypair);
    }

    /**
     * Generate a new random X25519 keypair.
     *
     * @return string A 64-byte string; the first 32 are your secret key, while
     *                the last 32 are your public key. crypto_box_secretkey()
     *                and crypto_box_publickey() exist to separate them so you
     *                don't accidentally get them mixed up!
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_box_keypair()
    {
        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_box_keypair();
        }
        if (self::use_fallback('crypto_box_keypair')) {
            return (string) call_user_func('\\Sodium\\crypto_box_keypair');
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::box_keypair();
        }
        return ParagonIE_Sodium_Crypto::box_keypair();
    }

    /**
     * Combine two keys into a keypair for use in library methods that expect
     * a keypair. This doesn't necessarily have to be the same person's keys.
     *
     * @param string $secretKey Secret key
     * @param string $publicKey Public key
     * @return string    Keypair
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_box_keypair_from_secretkey_and_publickey($secretKey, $publicKey)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($secretKey, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($publicKey, 'string', 2);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($secretKey) !== self::CRYPTO_BOX_SECRETKEYBYTES) {
            throw new SodiumException('Argument 1 must be CRYPTO_BOX_SECRETKEYBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($publicKey) !== self::CRYPTO_BOX_PUBLICKEYBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_BOX_PUBLICKEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_box_keypair_from_secretkey_and_publickey($secretKey, $publicKey);
        }
        if (self::use_fallback('crypto_box_keypair_from_secretkey_and_publickey')) {
            return (string) call_user_func('\\Sodium\\crypto_box_keypair_from_secretkey_and_publickey', $secretKey, $publicKey);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::box_keypair_from_secretkey_and_publickey($secretKey, $publicKey);
        }
        return ParagonIE_Sodium_Crypto::box_keypair_from_secretkey_and_publickey($secretKey, $publicKey);
    }

    /**
     * Decrypt a message previously encrypted with crypto_box().
     *
     * @param string $ciphertext Encrypted message
     * @param string $nonce      Number to only be used Once; must be 24 bytes
     * @param string $keypair    Your secret key and the sender's public key
     * @return string            The original plaintext message
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedInferredReturnType
     * @psalm-suppress MixedReturnStatement
     */
    public static function crypto_box_open($ciphertext, $nonce, $keypair)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($ciphertext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($keypair, 'string', 3);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($ciphertext) < self::CRYPTO_BOX_MACBYTES) {
            throw new SodiumException('Argument 1 must be at least CRYPTO_BOX_MACBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_BOX_NONCEBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_BOX_NONCEBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($keypair) !== self::CRYPTO_BOX_KEYPAIRBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_BOX_KEYPAIRBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            /**
             * @psalm-suppress InvalidReturnStatement
             * @psalm-suppress FalsableReturnStatement
             */
            return sodium_crypto_box_open($ciphertext, $nonce, $keypair);
        }
        if (self::use_fallback('crypto_box_open')) {
            return call_user_func('\\Sodium\\crypto_box_open', $ciphertext, $nonce, $keypair);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::box_open($ciphertext, $nonce, $keypair);
        }
        return ParagonIE_Sodium_Crypto::box_open($ciphertext, $nonce, $keypair);
    }

    /**
     * Extract the public key from a crypto_box keypair.
     *
     * @param string $keypair Keypair containing secret and public key
     * @return string         Your crypto_box public key
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_box_publickey($keypair)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($keypair, 'string', 1);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($keypair) !== self::CRYPTO_BOX_KEYPAIRBYTES) {
            throw new SodiumException('Argument 1 must be CRYPTO_BOX_KEYPAIRBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_box_publickey($keypair);
        }
        if (self::use_fallback('crypto_box_publickey')) {
            return (string) call_user_func('\\Sodium\\crypto_box_publickey', $keypair);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::box_publickey($keypair);
        }
        return ParagonIE_Sodium_Crypto::box_publickey($keypair);
    }

    /**
     * Calculate the X25519 public key from a given X25519 secret key.
     *
     * @param string $secretKey Any X25519 secret key
     * @return string           The corresponding X25519 public key
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_box_publickey_from_secretkey($secretKey)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($secretKey, 'string', 1);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($secretKey) !== self::CRYPTO_BOX_SECRETKEYBYTES) {
            throw new SodiumException('Argument 1 must be CRYPTO_BOX_SECRETKEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_box_publickey_from_secretkey($secretKey);
        }
        if (self::use_fallback('crypto_box_publickey_from_secretkey')) {
            return (string) call_user_func('\\Sodium\\crypto_box_publickey_from_secretkey', $secretKey);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::box_publickey_from_secretkey($secretKey);
        }
        return ParagonIE_Sodium_Crypto::box_publickey_from_secretkey($secretKey);
    }

    /**
     * Extract the secret key from a crypto_box keypair.
     *
     * @param string $keypair
     * @return string         Your crypto_box secret key
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_box_secretkey($keypair)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($keypair, 'string', 1);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($keypair) !== self::CRYPTO_BOX_KEYPAIRBYTES) {
            throw new SodiumException('Argument 1 must be CRYPTO_BOX_KEYPAIRBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_box_secretkey($keypair);
        }
        if (self::use_fallback('crypto_box_secretkey')) {
            return (string) call_user_func('\\Sodium\\crypto_box_secretkey', $keypair);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::box_secretkey($keypair);
        }
        return ParagonIE_Sodium_Crypto::box_secretkey($keypair);
    }

    /**
     * Generate an X25519 keypair from a seed.
     *
     * @param string $seed
     * @return string
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress UndefinedFunction
     */
    public static function crypto_box_seed_keypair($seed)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($seed, 'string', 1);

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_box_seed_keypair($seed);
        }
        if (self::use_fallback('crypto_box_seed_keypair')) {
            return (string) call_user_func('\\Sodium\\crypto_box_seed_keypair', $seed);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::box_seed_keypair($seed);
        }
        return ParagonIE_Sodium_Crypto::box_seed_keypair($seed);
    }

    /**
     * Calculates a BLAKE2b hash, with an optional key.
     *
     * @param string      $message The message to be hashed
     * @param string|null $key     If specified, must be a string between 16
     *                             and 64 bytes long
     * @param int         $length  Output length in bytes; must be between 16
     *                             and 64 (default = 32)
     * @return string              Raw binary
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_generichash($message, $key = '', $length = self::CRYPTO_GENERICHASH_BYTES)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($message, 'string', 1);
        if (is_null($key)) {
            $key = '';
        }
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($length, 'int', 3);

        /* Input validation: */
        if (!empty($key)) {
            if (ParagonIE_Sodium_Core_Util::strlen($key) < self::CRYPTO_GENERICHASH_KEYBYTES_MIN) {
                throw new SodiumException('Unsupported key size. Must be at least CRYPTO_GENERICHASH_KEYBYTES_MIN bytes long.');
            }
            if (ParagonIE_Sodium_Core_Util::strlen($key) > self::CRYPTO_GENERICHASH_KEYBYTES_MAX) {
                throw new SodiumException('Unsupported key size. Must be at most CRYPTO_GENERICHASH_KEYBYTES_MAX bytes long.');
            }
        }

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_generichash($message, $key, $length);
        }
        if (self::use_fallback('crypto_generichash')) {
            return (string) call_user_func('\\Sodium\\crypto_generichash', $message, $key, $length);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::generichash($message, $key, $length);
        }
        return ParagonIE_Sodium_Crypto::generichash($message, $key, $length);
    }

    /**
     * Get the final BLAKE2b hash output for a given context.
     *
     * @param string $ctx BLAKE2 hashing context. Generated by crypto_generichash_init().
     * @param int $length Hash output size.
     * @return string     Final BLAKE2b hash.
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress ReferenceConstraintViolation
     * @psalm-suppress ConflictingReferenceConstraint
     */
    public static function crypto_generichash_final(&$ctx, $length = self::CRYPTO_GENERICHASH_BYTES)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($ctx, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($length, 'int', 2);

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_generichash_final($ctx, $length);
        }
        if (self::use_fallback('crypto_generichash_final')) {
            $func = '\\Sodium\\crypto_generichash_final';
            return (string) $func($ctx, $length);
        }
        if ($length < 1) {
            try {
                self::memzero($ctx);
            } catch (SodiumException $ex) {
                unset($ctx);
            }
            return '';
        }
        if (PHP_INT_SIZE === 4) {
            $result = ParagonIE_Sodium_Crypto32::generichash_final($ctx, $length);
        } else {
            $result = ParagonIE_Sodium_Crypto::generichash_final($ctx, $length);
        }
        try {
            self::memzero($ctx);
        } catch (SodiumException $ex) {
            unset($ctx);
        }
        return $result;
    }

    /**
     * Initialize a BLAKE2b hashing context, for use in a streaming interface.
     *
     * @param string|null $key If specified must be a string between 16 and 64 bytes
     * @param int $length      The size of the desired hash output
     * @return string          A BLAKE2 hashing context, encoded as a string
     *                         (To be 100% compatible with ext/libsodium)
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_generichash_init($key = '', $length = self::CRYPTO_GENERICHASH_BYTES)
    {
        /* Type checks: */
        if (is_null($key)) {
            $key = '';
        }
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($length, 'int', 2);

        /* Input validation: */
        if (!empty($key)) {
            if (ParagonIE_Sodium_Core_Util::strlen($key) < self::CRYPTO_GENERICHASH_KEYBYTES_MIN) {
                throw new SodiumException('Unsupported key size. Must be at least CRYPTO_GENERICHASH_KEYBYTES_MIN bytes long.');
            }
            if (ParagonIE_Sodium_Core_Util::strlen($key) > self::CRYPTO_GENERICHASH_KEYBYTES_MAX) {
                throw new SodiumException('Unsupported key size. Must be at most CRYPTO_GENERICHASH_KEYBYTES_MAX bytes long.');
            }
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_generichash_init($key, $length);
        }
        if (self::use_fallback('crypto_generichash_init')) {
            return (string) call_user_func('\\Sodium\\crypto_generichash_init', $key, $length);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::generichash_init($key, $length);
        }
        return ParagonIE_Sodium_Crypto::generichash_init($key, $length);
    }

    /**
     * Initialize a BLAKE2b hashing context, for use in a streaming interface.
     *
     * @param string|null $key If specified must be a string between 16 and 64 bytes
     * @param int $length      The size of the desired hash output
     * @param string $salt     Salt (up to 16 bytes)
     * @param string $personal Personalization string (up to 16 bytes)
     * @return string          A BLAKE2 hashing context, encoded as a string
     *                         (To be 100% compatible with ext/libsodium)
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_generichash_init_salt_personal(
        $key = '',
        $length = self::CRYPTO_GENERICHASH_BYTES,
        $salt = '',
        $personal = ''
    ) {
        /* Type checks: */
        if (is_null($key)) {
            $key = '';
        }
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($length, 'int', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($salt, 'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($personal, 'string', 4);
        $salt = str_pad($salt, 16, "\0", STR_PAD_RIGHT);
        $personal = str_pad($personal, 16, "\0", STR_PAD_RIGHT);

        /* Input validation: */
        if (!empty($key)) {
            /*
            if (ParagonIE_Sodium_Core_Util::strlen($key) < self::CRYPTO_GENERICHASH_KEYBYTES_MIN) {
                throw new SodiumException('Unsupported key size. Must be at least CRYPTO_GENERICHASH_KEYBYTES_MIN bytes long.');
            }
            */
            if (ParagonIE_Sodium_Core_Util::strlen($key) > self::CRYPTO_GENERICHASH_KEYBYTES_MAX) {
                throw new SodiumException('Unsupported key size. Must be at most CRYPTO_GENERICHASH_KEYBYTES_MAX bytes long.');
            }
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::generichash_init_salt_personal($key, $length, $salt, $personal);
        }
        return ParagonIE_Sodium_Crypto::generichash_init_salt_personal($key, $length, $salt, $personal);
    }

    /**
     * Update a BLAKE2b hashing context with additional data.
     *
     * @param string $ctx    BLAKE2 hashing context. Generated by crypto_generichash_init().
     *                       $ctx is passed by reference and gets updated in-place.
     * @param-out string $ctx
     * @param string $message The message to append to the existing hash state.
     * @return void
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress ReferenceConstraintViolation
     */
    public static function crypto_generichash_update(&$ctx, $message)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($ctx, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($message, 'string', 2);

        if (self::useNewSodiumAPI()) {
            sodium_crypto_generichash_update($ctx, $message);
            return;
        }
        if (self::use_fallback('crypto_generichash_update')) {
            $func = '\\Sodium\\crypto_generichash_update';
            $func($ctx, $message);
            return;
        }
        if (PHP_INT_SIZE === 4) {
            $ctx = ParagonIE_Sodium_Crypto32::generichash_update($ctx, $message);
        } else {
            $ctx = ParagonIE_Sodium_Crypto::generichash_update($ctx, $message);
        }
    }

    /**
     * @return string
     * @throws Exception
     * @throws Error
     */
    public static function crypto_generichash_keygen()
    {
        return random_bytes(self::CRYPTO_GENERICHASH_KEYBYTES);
    }

    /**
     * @param int $subkey_len
     * @param int $subkey_id
     * @param string $context
     * @param string $key
     * @return string
     * @throws SodiumException
     */
    public static function crypto_kdf_derive_from_key(
        $subkey_len,
        $subkey_id,
        $context,
        $key
    ) {
        ParagonIE_Sodium_Core_Util::declareScalarType($subkey_len, 'int', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($subkey_id, 'int', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($context, 'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4);
        $subkey_id = (int) $subkey_id;
        $subkey_len = (int) $subkey_len;
        $context = (string) $context;
        $key = (string) $key;

        if ($subkey_len < self::CRYPTO_KDF_BYTES_MIN) {
            throw new SodiumException('subkey cannot be smaller than SODIUM_CRYPTO_KDF_BYTES_MIN');
        }
        if ($subkey_len > self::CRYPTO_KDF_BYTES_MAX) {
            throw new SodiumException('subkey cannot be larger than SODIUM_CRYPTO_KDF_BYTES_MAX');
        }
        if ($subkey_id < 0) {
            throw new SodiumException('subkey_id cannot be negative');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($context) !== self::CRYPTO_KDF_CONTEXTBYTES) {
            throw new SodiumException('context should be SODIUM_CRYPTO_KDF_CONTEXTBYTES bytes');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_KDF_KEYBYTES) {
            throw new SodiumException('key should be SODIUM_CRYPTO_KDF_KEYBYTES bytes');
        }

        $salt = ParagonIE_Sodium_Core_Util::store64_le($subkey_id);
        $state = self::crypto_generichash_init_salt_personal(
            $key,
            $subkey_len,
            $salt,
            $context
        );
        return self::crypto_generichash_final($state, $subkey_len);
    }

    /**
     * @return string
     * @throws Exception
     * @throws Error
     */
    public static function crypto_kdf_keygen()
    {
        return random_bytes(self::CRYPTO_KDF_KEYBYTES);
    }

    /**
     * Perform a key exchange, between a designated client and a server.
     *
     * Typically, you would designate one machine to be the client and the
     * other to be the server. The first two keys are what you'd expect for
     * scalarmult() below, but the latter two public keys don't swap places.
     *
     * | ALICE                          | BOB                                 |
     * | Client                         | Server                              |
     * |--------------------------------|-------------------------------------|
     * | shared = crypto_kx(            | shared = crypto_kx(                 |
     * |     alice_sk,                  |     bob_sk,                         | <- contextual
     * |     bob_pk,                    |     alice_pk,                       | <- contextual
     * |     alice_pk,                  |     alice_pk,                       | <----- static
     * |     bob_pk                     |     bob_pk                          | <----- static
     * | )                              | )                                   |
     *
     * They are used along with the scalarmult product to generate a 256-bit
     * BLAKE2b hash unique to the client and server keys.
     *
     * @param string $my_secret
     * @param string $their_public
     * @param string $client_public
     * @param string $server_public
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_kx($my_secret, $their_public, $client_public, $server_public, $dontFallback = false)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($my_secret, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($their_public, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($client_public, 'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($server_public, 'string', 4);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($my_secret) !== self::CRYPTO_BOX_SECRETKEYBYTES) {
            throw new SodiumException('Argument 1 must be CRYPTO_BOX_SECRETKEYBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($their_public) !== self::CRYPTO_BOX_PUBLICKEYBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_BOX_PUBLICKEYBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($client_public) !== self::CRYPTO_BOX_PUBLICKEYBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_BOX_PUBLICKEYBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($server_public) !== self::CRYPTO_BOX_PUBLICKEYBYTES) {
            throw new SodiumException('Argument 4 must be CRYPTO_BOX_PUBLICKEYBYTES long.');
        }

        if (self::useNewSodiumAPI() && !$dontFallback) {
            if (is_callable('sodium_crypto_kx')) {
                return (string) sodium_crypto_kx(
                    $my_secret,
                    $their_public,
                    $client_public,
                    $server_public
                );
            }
        }
        if (self::use_fallback('crypto_kx')) {
            return (string) call_user_func(
                '\\Sodium\\crypto_kx',
                $my_secret,
                $their_public,
                $client_public,
                $server_public
            );
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::keyExchange(
                $my_secret,
                $their_public,
                $client_public,
                $server_public
            );
        }
        return ParagonIE_Sodium_Crypto::keyExchange(
            $my_secret,
            $their_public,
            $client_public,
            $server_public
        );
    }

    /**
     * @param string $seed
     * @return string
     * @throws SodiumException
     */
    public static function crypto_kx_seed_keypair($seed)
    {
        ParagonIE_Sodium_Core_Util::declareScalarType($seed, 'string', 1);

        $seed = (string) $seed;

        if (ParagonIE_Sodium_Core_Util::strlen($seed) !== self::CRYPTO_KX_SEEDBYTES) {
            throw new SodiumException('seed must be SODIUM_CRYPTO_KX_SEEDBYTES bytes');
        }

        $sk = self::crypto_generichash($seed, '', self::CRYPTO_KX_SECRETKEYBYTES);
        $pk = self::crypto_scalarmult_base($sk);
        return $sk . $pk;
    }

    /**
     * @return string
     * @throws Exception
     */
    public static function crypto_kx_keypair()
    {
        $sk = self::randombytes_buf(self::CRYPTO_KX_SECRETKEYBYTES);
        $pk = self::crypto_scalarmult_base($sk);
        return $sk . $pk;
    }

    /**
     * @param string $keypair
     * @param string $serverPublicKey
     * @return array{0: string, 1: string}
     * @throws SodiumException
     */
    public static function crypto_kx_client_session_keys($keypair, $serverPublicKey)
    {
        ParagonIE_Sodium_Core_Util::declareScalarType($keypair, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($serverPublicKey, 'string', 2);

        $keypair = (string) $keypair;
        $serverPublicKey = (string) $serverPublicKey;

        if (ParagonIE_Sodium_Core_Util::strlen($keypair) !== self::CRYPTO_KX_KEYPAIRBYTES) {
            throw new SodiumException('keypair should be SODIUM_CRYPTO_KX_KEYPAIRBYTES bytes');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($serverPublicKey) !== self::CRYPTO_KX_PUBLICKEYBYTES) {
            throw new SodiumException('public keys must be SODIUM_CRYPTO_KX_PUBLICKEYBYTES bytes');
        }

        $sk = self::crypto_kx_secretkey($keypair);
        $pk = self::crypto_kx_publickey($keypair);
        $h = self::crypto_generichash_init(null, self::CRYPTO_KX_SESSIONKEYBYTES * 2);
        self::crypto_generichash_update($h, self::crypto_scalarmult($sk, $serverPublicKey));
        self::crypto_generichash_update($h, $pk);
        self::crypto_generichash_update($h, $serverPublicKey);
        $sessionKeys = self::crypto_generichash_final($h, self::CRYPTO_KX_SESSIONKEYBYTES * 2);
        return array(
            ParagonIE_Sodium_Core_Util::substr(
                $sessionKeys,
                0,
                self::CRYPTO_KX_SESSIONKEYBYTES
            ),
            ParagonIE_Sodium_Core_Util::substr(
                $sessionKeys,
                self::CRYPTO_KX_SESSIONKEYBYTES,
                self::CRYPTO_KX_SESSIONKEYBYTES
            )
        );
    }

    /**
     * @param string $keypair
     * @param string $clientPublicKey
     * @return array{0: string, 1: string}
     * @throws SodiumException
     */
    public static function crypto_kx_server_session_keys($keypair, $clientPublicKey)
    {
        ParagonIE_Sodium_Core_Util::declareScalarType($keypair, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($clientPublicKey, 'string', 2);

        $keypair = (string) $keypair;
        $clientPublicKey = (string) $clientPublicKey;

        if (ParagonIE_Sodium_Core_Util::strlen($keypair) !== self::CRYPTO_KX_KEYPAIRBYTES) {
            throw new SodiumException('keypair should be SODIUM_CRYPTO_KX_KEYPAIRBYTES bytes');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($clientPublicKey) !== self::CRYPTO_KX_PUBLICKEYBYTES) {
            throw new SodiumException('public keys must be SODIUM_CRYPTO_KX_PUBLICKEYBYTES bytes');
        }

        $sk = self::crypto_kx_secretkey($keypair);
        $pk = self::crypto_kx_publickey($keypair);
        $h = self::crypto_generichash_init(null, self::CRYPTO_KX_SESSIONKEYBYTES * 2);
        self::crypto_generichash_update($h, self::crypto_scalarmult($sk, $clientPublicKey));
        self::crypto_generichash_update($h, $clientPublicKey);
        self::crypto_generichash_update($h, $pk);
        $sessionKeys = self::crypto_generichash_final($h, self::CRYPTO_KX_SESSIONKEYBYTES * 2);
        return array(
            ParagonIE_Sodium_Core_Util::substr(
                $sessionKeys,
                self::CRYPTO_KX_SESSIONKEYBYTES,
                self::CRYPTO_KX_SESSIONKEYBYTES
            ),
            ParagonIE_Sodium_Core_Util::substr(
                $sessionKeys,
                0,
                self::CRYPTO_KX_SESSIONKEYBYTES
            )
        );
    }

    /**
     * @param string $kp
     * @return string
     * @throws SodiumException
     */
    public static function crypto_kx_secretkey($kp)
    {
        return ParagonIE_Sodium_Core_Util::substr(
            $kp,
            0,
            self::CRYPTO_KX_SECRETKEYBYTES
        );
    }

    /**
     * @param string $kp
     * @return string
     * @throws SodiumException
     */
    public static function crypto_kx_publickey($kp)
    {
        return ParagonIE_Sodium_Core_Util::substr(
            $kp,
            self::CRYPTO_KX_SECRETKEYBYTES,
            self::CRYPTO_KX_PUBLICKEYBYTES
        );
    }

    /**
     * @param int $outlen
     * @param string $passwd
     * @param string $salt
     * @param int $opslimit
     * @param int $memlimit
     * @param int|null $alg
     * @return string
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_pwhash($outlen, $passwd, $salt, $opslimit, $memlimit, $alg = null)
    {
        ParagonIE_Sodium_Core_Util::declareScalarType($outlen, 'int', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($passwd, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($salt,  'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($opslimit, 'int', 4);
        ParagonIE_Sodium_Core_Util::declareScalarType($memlimit, 'int', 5);

        if (self::useNewSodiumAPI()) {
            if (!is_null($alg)) {
                ParagonIE_Sodium_Core_Util::declareScalarType($alg, 'int', 6);
                return sodium_crypto_pwhash($outlen, $passwd, $salt, $opslimit, $memlimit, $alg);
            }
            return sodium_crypto_pwhash($outlen, $passwd, $salt, $opslimit, $memlimit);
        }
        if (self::use_fallback('crypto_pwhash')) {
            return (string) call_user_func('\\Sodium\\crypto_pwhash', $outlen, $passwd, $salt, $opslimit, $memlimit);
        }
        // This is the best we can do.
        throw new SodiumException(
            'This is not implemented, as it is not possible to implement Argon2i with acceptable performance in pure-PHP'
        );
    }

    /**
     * !Exclusive to sodium_compat!
     *
     * This returns TRUE if the native crypto_pwhash API is available by libsodium.
     * This returns FALSE if only sodium_compat is available.
     *
     * @return bool
     */
    public static function crypto_pwhash_is_available()
    {
        if (self::useNewSodiumAPI()) {
            return true;
        }
        if (self::use_fallback('crypto_pwhash')) {
            return true;
        }
        return false;
    }

    /**
     * @param string $passwd
     * @param int $opslimit
     * @param int $memlimit
     * @return string
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_pwhash_str($passwd, $opslimit, $memlimit)
    {
        ParagonIE_Sodium_Core_Util::declareScalarType($passwd, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($opslimit, 'int', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($memlimit, 'int', 3);

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_pwhash_str($passwd, $opslimit, $memlimit);
        }
        if (self::use_fallback('crypto_pwhash_str')) {
            return (string) call_user_func('\\Sodium\\crypto_pwhash_str', $passwd, $opslimit, $memlimit);
        }
        // This is the best we can do.
        throw new SodiumException(
            'This is not implemented, as it is not possible to implement Argon2i with acceptable performance in pure-PHP'
        );
    }

    /**
     * Do we need to rehash this password?
     *
     * @param string $hash
     * @param int $opslimit
     * @param int $memlimit
     * @return bool
     * @throws SodiumException
     */
    public static function crypto_pwhash_str_needs_rehash($hash, $opslimit, $memlimit)
    {
        ParagonIE_Sodium_Core_Util::declareScalarType($hash, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($opslimit, 'int', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($memlimit, 'int', 3);

        // Just grab the first 4 pieces.
        $pieces = explode('$', (string) $hash);
        $prefix = implode('$', array_slice($pieces, 0, 4));

        // Rebuild the expected header.
        /** @var int $ops */
        $ops = (int) $opslimit;
        /** @var int $mem */
        $mem = (int) $memlimit >> 10;
        $encoded = self::CRYPTO_PWHASH_STRPREFIX . 'v=19$m=' . $mem . ',t=' . $ops . ',p=1';

        // Do they match? If so, we don't need to rehash, so return false.
        return !ParagonIE_Sodium_Core_Util::hashEquals($encoded, $prefix);
    }

    /**
     * @param string $passwd
     * @param string $hash
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_pwhash_str_verify($passwd, $hash)
    {
        ParagonIE_Sodium_Core_Util::declareScalarType($passwd, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($hash, 'string', 2);

        if (self::useNewSodiumAPI()) {
            return (bool) sodium_crypto_pwhash_str_verify($passwd, $hash);
        }
        if (self::use_fallback('crypto_pwhash_str_verify')) {
            return (bool) call_user_func('\\Sodium\\crypto_pwhash_str_verify', $passwd, $hash);
        }
        // This is the best we can do.
        throw new SodiumException(
            'This is not implemented, as it is not possible to implement Argon2i with acceptable performance in pure-PHP'
        );
    }

    /**
     * @param int $outlen
     * @param string $passwd
     * @param string $salt
     * @param int $opslimit
     * @param int $memlimit
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function crypto_pwhash_scryptsalsa208sha256($outlen, $passwd, $salt, $opslimit, $memlimit)
    {
        ParagonIE_Sodium_Core_Util::declareScalarType($outlen, 'int', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($passwd, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($salt,  'string', 3);
        ParagonIE_Sodium_Core_Util::declareScalarType($opslimit, 'int', 4);
        ParagonIE_Sodium_Core_Util::declareScalarType($memlimit, 'int', 5);

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_pwhash_scryptsalsa208sha256(
                (int) $outlen,
                (string) $passwd,
                (string) $salt,
                (int) $opslimit,
                (int) $memlimit
            );
        }
        if (self::use_fallback('crypto_pwhash_scryptsalsa208sha256')) {
            return (string) call_user_func(
                '\\Sodium\\crypto_pwhash_scryptsalsa208sha256',
                (int) $outlen,
                (string) $passwd,
                (string) $salt,
                (int) $opslimit,
                (int) $memlimit
            );
        }
        // This is the best we can do.
        throw new SodiumException(
            'This is not implemented, as it is not possible to implement Scrypt with acceptable performance in pure-PHP'
        );
    }

    /**
     * !Exclusive to sodium_compat!
     *
     * This returns TRUE if the native crypto_pwhash API is available by libsodium.
     * This returns FALSE if only sodium_compat is available.
     *
     * @return bool
     */
    public static function crypto_pwhash_scryptsalsa208sha256_is_available()
    {
        if (self::useNewSodiumAPI()) {
            return true;
        }
        if (self::use_fallback('crypto_pwhash_scryptsalsa208sha256')) {
            return true;
        }
        return false;
    }

    /**
     * @param string $passwd
     * @param int $opslimit
     * @param int $memlimit
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function crypto_pwhash_scryptsalsa208sha256_str($passwd, $opslimit, $memlimit)
    {
        ParagonIE_Sodium_Core_Util::declareScalarType($passwd, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($opslimit, 'int', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($memlimit, 'int', 3);

        if (self::useNewSodiumAPI()) {
            return (string) sodium_crypto_pwhash_scryptsalsa208sha256_str(
                (string) $passwd,
                (int) $opslimit,
                (int) $memlimit
            );
        }
        if (self::use_fallback('crypto_pwhash_scryptsalsa208sha256_str')) {
            return (string) call_user_func(
                '\\Sodium\\crypto_pwhash_scryptsalsa208sha256_str',
                (string) $passwd,
                (int) $opslimit,
                (int) $memlimit
            );
        }
        // This is the best we can do.
        throw new SodiumException(
            'This is not implemented, as it is not possible to implement Scrypt with acceptable performance in pure-PHP'
        );
    }

    /**
     * @param string $passwd
     * @param string $hash
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function crypto_pwhash_scryptsalsa208sha256_str_verify($passwd, $hash)
    {
        ParagonIE_Sodium_Core_Util::declareScalarType($passwd, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($hash, 'string', 2);

        if (self::useNewSodiumAPI()) {
            return (bool) sodium_crypto_pwhash_scryptsalsa208sha256_str_verify(
                (string) $passwd,
                (string) $hash
            );
        }
        if (self::use_fallback('crypto_pwhash_scryptsalsa208sha256_str_verify')) {
            return (bool) call_user_func(
                '\\Sodium\\crypto_pwhash_scryptsalsa208sha256_str_verify',
                (string) $passwd,
                (string) $hash
            );
        }
        // This is the best we can do.
        throw new SodiumException(
            'This is not implemented, as it is not possible to implement Scrypt with acceptable performance in pure-PHP'
        );
    }

    /**
     * Calculate the shared secret between your secret key and your
     * recipient's public key.
     *
     * Algorithm: X25519 (ECDH over Curve25519)
     *
     * @param string $secretKey
     * @param string $publicKey
     * @return string
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_scalarmult($secretKey, $publicKey)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($secretKey, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($publicKey, 'string', 2);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($secretKey) !== self::CRYPTO_BOX_SECRETKEYBYTES) {
            throw new SodiumException('Argument 1 must be CRYPTO_BOX_SECRETKEYBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($publicKey) !== self::CRYPTO_BOX_PUBLICKEYBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_BOX_PUBLICKEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_scalarmult($secretKey, $publicKey);
        }
        if (self::use_fallback('crypto_scalarmult')) {
            return (string) call_user_func('\\Sodium\\crypto_scalarmult', $secretKey, $publicKey);
        }

        /* Output validation: Forbid all-zero keys */
        if (ParagonIE_Sodium_Core_Util::hashEquals($secretKey, str_repeat("\0", self::CRYPTO_BOX_SECRETKEYBYTES))) {
            throw new SodiumException('Zero secret key is not allowed');
        }
        if (ParagonIE_Sodium_Core_Util::hashEquals($publicKey, str_repeat("\0", self::CRYPTO_BOX_PUBLICKEYBYTES))) {
            throw new SodiumException('Zero public key is not allowed');
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::scalarmult($secretKey, $publicKey);
        }
        return ParagonIE_Sodium_Crypto::scalarmult($secretKey, $publicKey);
    }

    /**
     * Calculate an X25519 public key from an X25519 secret key.
     *
     * @param string $secretKey
     * @return string
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress TooFewArguments
     * @psalm-suppress MixedArgument
     */
    public static function crypto_scalarmult_base($secretKey)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($secretKey, 'string', 1);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($secretKey) !== self::CRYPTO_BOX_SECRETKEYBYTES) {
            throw new SodiumException('Argument 1 must be CRYPTO_BOX_SECRETKEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_scalarmult_base($secretKey);
        }
        if (self::use_fallback('crypto_scalarmult_base')) {
            return (string) call_user_func('\\Sodium\\crypto_scalarmult_base', $secretKey);
        }
        if (ParagonIE_Sodium_Core_Util::hashEquals($secretKey, str_repeat("\0", self::CRYPTO_BOX_SECRETKEYBYTES))) {
            throw new SodiumException('Zero secret key is not allowed');
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::scalarmult_base($secretKey);
        }
        return ParagonIE_Sodium_Crypto::scalarmult_base($secretKey);
    }

    /**
     * Authenticated symmetric-key encryption.
     *
     * Algorithm: XSalsa20-Poly1305
     *
     * @param string $plaintext The message you're encrypting
     * @param string $nonce A Number to be used Once; must be 24 bytes
     * @param string $key Symmetric encryption key
     * @return string           Ciphertext with Poly1305 MAC
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_secretbox($plaintext, $nonce, $key)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($plaintext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 3);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_SECRETBOX_NONCEBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_SECRETBOX_NONCEBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_SECRETBOX_KEYBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_SECRETBOX_KEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_secretbox($plaintext, $nonce, $key);
        }
        if (self::use_fallback('crypto_secretbox')) {
            return (string) call_user_func('\\Sodium\\crypto_secretbox', $plaintext, $nonce, $key);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::secretbox($plaintext, $nonce, $key);
        }
        return ParagonIE_Sodium_Crypto::secretbox($plaintext, $nonce, $key);
    }

    /**
     * Decrypts a message previously encrypted with crypto_secretbox().
     *
     * @param string $ciphertext Ciphertext with Poly1305 MAC
     * @param string $nonce      A Number to be used Once; must be 24 bytes
     * @param string $key        Symmetric encryption key
     * @return string            Original plaintext message
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedInferredReturnType
     * @psalm-suppress MixedReturnStatement
     */
    public static function crypto_secretbox_open($ciphertext, $nonce, $key)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($ciphertext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 3);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_SECRETBOX_NONCEBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_SECRETBOX_NONCEBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_SECRETBOX_KEYBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_SECRETBOX_KEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            /**
             * @psalm-suppress InvalidReturnStatement
             * @psalm-suppress FalsableReturnStatement
             */
            return sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
        }
        if (self::use_fallback('crypto_secretbox_open')) {
            return call_user_func('\\Sodium\\crypto_secretbox_open', $ciphertext, $nonce, $key);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::secretbox_open($ciphertext, $nonce, $key);
        }
        return ParagonIE_Sodium_Crypto::secretbox_open($ciphertext, $nonce, $key);
    }

    /**
     * Return a secure random key for use with crypto_secretbox
     *
     * @return string
     * @throws Exception
     * @throws Error
     */
    public static function crypto_secretbox_keygen()
    {
        return random_bytes(self::CRYPTO_SECRETBOX_KEYBYTES);
    }

    /**
     * Authenticated symmetric-key encryption.
     *
     * Algorithm: XChaCha20-Poly1305
     *
     * @param string $plaintext The message you're encrypting
     * @param string $nonce     A Number to be used Once; must be 24 bytes
     * @param string $key       Symmetric encryption key
     * @return string           Ciphertext with Poly1305 MAC
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_secretbox_xchacha20poly1305($plaintext, $nonce, $key)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($plaintext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 3);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_SECRETBOX_NONCEBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_SECRETBOX_NONCEBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_SECRETBOX_KEYBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_SECRETBOX_KEYBYTES long.');
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::secretbox_xchacha20poly1305($plaintext, $nonce, $key);
        }
        return ParagonIE_Sodium_Crypto::secretbox_xchacha20poly1305($plaintext, $nonce, $key);
    }
    /**
     * Decrypts a message previously encrypted with crypto_secretbox_xchacha20poly1305().
     *
     * @param string $ciphertext Ciphertext with Poly1305 MAC
     * @param string $nonce      A Number to be used Once; must be 24 bytes
     * @param string $key        Symmetric encryption key
     * @return string            Original plaintext message
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_secretbox_xchacha20poly1305_open($ciphertext, $nonce, $key)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($ciphertext, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 3);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_SECRETBOX_NONCEBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_SECRETBOX_NONCEBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_SECRETBOX_KEYBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_SECRETBOX_KEYBYTES long.');
        }

        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::secretbox_xchacha20poly1305_open($ciphertext, $nonce, $key);
        }
        return ParagonIE_Sodium_Crypto::secretbox_xchacha20poly1305_open($ciphertext, $nonce, $key);
    }

    /**
     * @param string $key
     * @return array<int, string> Returns a state and a header.
     * @throws Exception
     * @throws SodiumException
     */
    public static function crypto_secretstream_xchacha20poly1305_init_push($key)
    {
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::secretstream_xchacha20poly1305_init_push($key);
        }
        return ParagonIE_Sodium_Crypto::secretstream_xchacha20poly1305_init_push($key);
    }

    /**
     * @param string $header
     * @param string $key
     * @return string Returns a state.
     * @throws Exception
     */
    public static function crypto_secretstream_xchacha20poly1305_init_pull($header, $key)
    {
        if (ParagonIE_Sodium_Core_Util::strlen($header) < self::CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES) {
            throw new SodiumException(
                'header size should be SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES bytes'
            );
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::secretstream_xchacha20poly1305_init_pull($key, $header);
        }
        return ParagonIE_Sodium_Crypto::secretstream_xchacha20poly1305_init_pull($key, $header);
    }

    /**
     * @param string $state
     * @param string $msg
     * @param string $aad
     * @param int $tag
     * @return string
     * @throws SodiumException
     */
    public static function crypto_secretstream_xchacha20poly1305_push(&$state, $msg, $aad = '', $tag = 0)
    {
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::secretstream_xchacha20poly1305_push(
                $state,
                $msg,
                $aad,
                $tag
            );
        }
        return ParagonIE_Sodium_Crypto::secretstream_xchacha20poly1305_push(
            $state,
            $msg,
            $aad,
            $tag
        );
    }

    /**
     * @param string $state
     * @param string $msg
     * @param string $aad
     * @return bool|array{0: string, 1: int}
     * @throws SodiumException
     */
    public static function crypto_secretstream_xchacha20poly1305_pull(&$state, $msg, $aad = '')
    {
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::secretstream_xchacha20poly1305_pull(
                $state,
                $msg,
                $aad
            );
        }
        return ParagonIE_Sodium_Crypto::secretstream_xchacha20poly1305_pull(
            $state,
            $msg,
            $aad
        );
    }

    /**
     * @return string
     * @throws Exception
     */
    public static function crypto_secretstream_xchacha20poly1305_keygen()
    {
        return random_bytes(self::CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_KEYBYTES);
    }

    /**
     * @param string $state
     * @return void
     * @throws SodiumException
     */
    public static function crypto_secretstream_xchacha20poly1305_rekey(&$state)
    {
        if (PHP_INT_SIZE === 4) {
            ParagonIE_Sodium_Crypto32::secretstream_xchacha20poly1305_rekey($state);
        } else {
            ParagonIE_Sodium_Crypto::secretstream_xchacha20poly1305_rekey($state);
        }
    }

    /**
     * Calculates a SipHash-2-4 hash of a message for a given key.
     *
     * @param string $message Input message
     * @param string $key SipHash-2-4 key
     * @return string         Hash
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedInferredReturnType
     * @psalm-suppress MixedReturnStatement
     */
    public static function crypto_shorthash($message, $key)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($message, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 2);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_SHORTHASH_KEYBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_SHORTHASH_KEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_shorthash($message, $key);
        }
        if (self::use_fallback('crypto_shorthash')) {
            return (string) call_user_func('\\Sodium\\crypto_shorthash', $message, $key);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Core32_SipHash::sipHash24($message, $key);
        }
        return ParagonIE_Sodium_Core_SipHash::sipHash24($message, $key);
    }

    /**
     * Return a secure random key for use with crypto_shorthash
     *
     * @return string
     * @throws Exception
     * @throws Error
     */
    public static function crypto_shorthash_keygen()
    {
        return random_bytes(self::CRYPTO_SHORTHASH_KEYBYTES);
    }

    /**
     * Returns a signed message. You probably want crypto_sign_detached()
     * instead, which only returns the signature.
     *
     * Algorithm: Ed25519 (EdDSA over Curve25519)
     *
     * @param string $message Message to be signed.
     * @param string $secretKey Secret signing key.
     * @return string           Signed message (signature is prefixed).
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedInferredReturnType
     * @psalm-suppress MixedReturnStatement
     */
    public static function crypto_sign($message, $secretKey)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($message, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($secretKey, 'string', 2);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($secretKey) !== self::CRYPTO_SIGN_SECRETKEYBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_SIGN_SECRETKEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_sign($message, $secretKey);
        }
        if (self::use_fallback('crypto_sign')) {
            return (string) call_user_func('\\Sodium\\crypto_sign', $message, $secretKey);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::sign($message, $secretKey);
        }
        return ParagonIE_Sodium_Crypto::sign($message, $secretKey);
    }

    /**
     * Validates a signed message then returns the message.
     *
     * @param string $signedMessage A signed message
     * @param string $publicKey A public key
     * @return string               The original message (if the signature is
     *                              valid for this public key)
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedInferredReturnType
     * @psalm-suppress MixedReturnStatement
     */
    public static function crypto_sign_open($signedMessage, $publicKey)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($signedMessage, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($publicKey, 'string', 2);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($signedMessage) < self::CRYPTO_SIGN_BYTES) {
            throw new SodiumException('Argument 1 must be at least CRYPTO_SIGN_BYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($publicKey) !== self::CRYPTO_SIGN_PUBLICKEYBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_SIGN_PUBLICKEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            /**
             * @psalm-suppress InvalidReturnStatement
             * @psalm-suppress FalsableReturnStatement
             */
            return sodium_crypto_sign_open($signedMessage, $publicKey);
        }
        if (self::use_fallback('crypto_sign_open')) {
            return call_user_func('\\Sodium\\crypto_sign_open', $signedMessage, $publicKey);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::sign_open($signedMessage, $publicKey);
        }
        return ParagonIE_Sodium_Crypto::sign_open($signedMessage, $publicKey);
    }

    /**
     * Generate a new random Ed25519 keypair.
     *
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function crypto_sign_keypair()
    {
        if (self::useNewSodiumAPI()) {
            return sodium_crypto_sign_keypair();
        }
        if (self::use_fallback('crypto_sign_keypair')) {
            return (string) call_user_func('\\Sodium\\crypto_sign_keypair');
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Core32_Ed25519::keypair();
        }
        return ParagonIE_Sodium_Core_Ed25519::keypair();
    }

    /**
     * @param string $sk
     * @param string $pk
     * @return string
     * @throws SodiumException
     */
    public static function crypto_sign_keypair_from_secretkey_and_publickey($sk, $pk)
    {
        ParagonIE_Sodium_Core_Util::declareScalarType($sk, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($pk, 'string', 1);
        $sk = (string) $sk;
        $pk = (string) $pk;

        if (ParagonIE_Sodium_Core_Util::strlen($sk) !== self::CRYPTO_SIGN_SECRETKEYBYTES) {
            throw new SodiumException('secretkey should be SODIUM_CRYPTO_SIGN_SECRETKEYBYTES bytes');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($pk) !== self::CRYPTO_SIGN_PUBLICKEYBYTES) {
            throw new SodiumException('publickey should be SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES bytes');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_sign_keypair_from_secretkey_and_publickey($sk, $pk);
        }
        return $sk . $pk;
    }

    /**
     * Generate an Ed25519 keypair from a seed.
     *
     * @param string $seed Input seed
     * @return string      Keypair
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_sign_seed_keypair($seed)
    {
        ParagonIE_Sodium_Core_Util::declareScalarType($seed, 'string', 1);

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_sign_seed_keypair($seed);
        }
        if (self::use_fallback('crypto_sign_keypair')) {
            return (string) call_user_func('\\Sodium\\crypto_sign_seed_keypair', $seed);
        }
        $publicKey = '';
        $secretKey = '';
        if (PHP_INT_SIZE === 4) {
            ParagonIE_Sodium_Core32_Ed25519::seed_keypair($publicKey, $secretKey, $seed);
        } else {
            ParagonIE_Sodium_Core_Ed25519::seed_keypair($publicKey, $secretKey, $seed);
        }
        return $secretKey . $publicKey;
    }

    /**
     * Extract an Ed25519 public key from an Ed25519 keypair.
     *
     * @param string $keypair Keypair
     * @return string         Public key
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_sign_publickey($keypair)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($keypair, 'string', 1);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($keypair) !== self::CRYPTO_SIGN_KEYPAIRBYTES) {
            throw new SodiumException('Argument 1 must be CRYPTO_SIGN_KEYPAIRBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_sign_publickey($keypair);
        }
        if (self::use_fallback('crypto_sign_publickey')) {
            return (string) call_user_func('\\Sodium\\crypto_sign_publickey', $keypair);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Core32_Ed25519::publickey($keypair);
        }
        return ParagonIE_Sodium_Core_Ed25519::publickey($keypair);
    }

    /**
     * Calculate an Ed25519 public key from an Ed25519 secret key.
     *
     * @param string $secretKey Your Ed25519 secret key
     * @return string           The corresponding Ed25519 public key
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_sign_publickey_from_secretkey($secretKey)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($secretKey, 'string', 1);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($secretKey) !== self::CRYPTO_SIGN_SECRETKEYBYTES) {
            throw new SodiumException('Argument 1 must be CRYPTO_SIGN_SECRETKEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_sign_publickey_from_secretkey($secretKey);
        }
        if (self::use_fallback('crypto_sign_publickey_from_secretkey')) {
            return (string) call_user_func('\\Sodium\\crypto_sign_publickey_from_secretkey', $secretKey);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Core32_Ed25519::publickey_from_secretkey($secretKey);
        }
        return ParagonIE_Sodium_Core_Ed25519::publickey_from_secretkey($secretKey);
    }

    /**
     * Extract an Ed25519 secret key from an Ed25519 keypair.
     *
     * @param string $keypair Keypair
     * @return string         Secret key
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_sign_secretkey($keypair)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($keypair, 'string', 1);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($keypair) !== self::CRYPTO_SIGN_KEYPAIRBYTES) {
            throw new SodiumException('Argument 1 must be CRYPTO_SIGN_KEYPAIRBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_sign_secretkey($keypair);
        }
        if (self::use_fallback('crypto_sign_secretkey')) {
            return (string) call_user_func('\\Sodium\\crypto_sign_secretkey', $keypair);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Core32_Ed25519::secretkey($keypair);
        }
        return ParagonIE_Sodium_Core_Ed25519::secretkey($keypair);
    }

    /**
     * Calculate the Ed25519 signature of a message and return ONLY the signature.
     *
     * Algorithm: Ed25519 (EdDSA over Curve25519)
     *
     * @param string $message Message to be signed
     * @param string $secretKey Secret signing key
     * @return string           Digital signature
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_sign_detached($message, $secretKey)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($message, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($secretKey, 'string', 2);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($secretKey) !== self::CRYPTO_SIGN_SECRETKEYBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_SIGN_SECRETKEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_sign_detached($message, $secretKey);
        }
        if (self::use_fallback('crypto_sign_detached')) {
            return (string) call_user_func('\\Sodium\\crypto_sign_detached', $message, $secretKey);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::sign_detached($message, $secretKey);
        }
        return ParagonIE_Sodium_Crypto::sign_detached($message, $secretKey);
    }

    /**
     * Verify the Ed25519 signature of a message.
     *
     * @param string $signature Digital sginature
     * @param string $message Message to be verified
     * @param string $publicKey Public key
     * @return bool             TRUE if this signature is good for this public key;
     *                          FALSE otherwise
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_sign_verify_detached($signature, $message, $publicKey)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($signature, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($message, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($publicKey, 'string', 3);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($signature) !== self::CRYPTO_SIGN_BYTES) {
            throw new SodiumException('Argument 1 must be CRYPTO_SIGN_BYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($publicKey) !== self::CRYPTO_SIGN_PUBLICKEYBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_SIGN_PUBLICKEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_sign_verify_detached($signature, $message, $publicKey);
        }
        if (self::use_fallback('crypto_sign_verify_detached')) {
            return (bool) call_user_func(
                '\\Sodium\\crypto_sign_verify_detached',
                $signature,
                $message,
                $publicKey
            );
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Crypto32::sign_verify_detached($signature, $message, $publicKey);
        }
        return ParagonIE_Sodium_Crypto::sign_verify_detached($signature, $message, $publicKey);
    }

    /**
     * Convert an Ed25519 public key to a Curve25519 public key
     *
     * @param string $pk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_sign_ed25519_pk_to_curve25519($pk)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($pk, 'string', 1);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($pk) < self::CRYPTO_SIGN_PUBLICKEYBYTES) {
            throw new SodiumException('Argument 1 must be at least CRYPTO_SIGN_PUBLICKEYBYTES long.');
        }
        if (self::useNewSodiumAPI()) {
            if (is_callable('crypto_sign_ed25519_pk_to_curve25519')) {
                return (string) sodium_crypto_sign_ed25519_pk_to_curve25519($pk);
            }
        }
        if (self::use_fallback('crypto_sign_ed25519_pk_to_curve25519')) {
            return (string) call_user_func('\\Sodium\\crypto_sign_ed25519_pk_to_curve25519', $pk);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Core32_Ed25519::pk_to_curve25519($pk);
        }
        return ParagonIE_Sodium_Core_Ed25519::pk_to_curve25519($pk);
    }

    /**
     * Convert an Ed25519 secret key to a Curve25519 secret key
     *
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_sign_ed25519_sk_to_curve25519($sk)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($sk, 'string', 1);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($sk) < self::CRYPTO_SIGN_SEEDBYTES) {
            throw new SodiumException('Argument 1 must be at least CRYPTO_SIGN_SEEDBYTES long.');
        }
        if (self::useNewSodiumAPI()) {
            if (is_callable('crypto_sign_ed25519_sk_to_curve25519')) {
                return sodium_crypto_sign_ed25519_sk_to_curve25519($sk);
            }
        }
        if (self::use_fallback('crypto_sign_ed25519_sk_to_curve25519')) {
            return (string) call_user_func('\\Sodium\\crypto_sign_ed25519_sk_to_curve25519', $sk);
        }

        $h = hash('sha512', ParagonIE_Sodium_Core_Util::substr($sk, 0, 32), true);
        $h[0] = ParagonIE_Sodium_Core_Util::intToChr(
            ParagonIE_Sodium_Core_Util::chrToInt($h[0]) & 248
        );
        $h[31] = ParagonIE_Sodium_Core_Util::intToChr(
            (ParagonIE_Sodium_Core_Util::chrToInt($h[31]) & 127) | 64
        );
        return ParagonIE_Sodium_Core_Util::substr($h, 0, 32);
    }

    /**
     * Expand a key and nonce into a keystream of pseudorandom bytes.
     *
     * @param int $len Number of bytes desired
     * @param string $nonce Number to be used Once; must be 24 bytes
     * @param string $key XSalsa20 key
     * @return string       Pseudorandom stream that can be XORed with messages
     *                      to provide encryption (but not authentication; see
     *                      Poly1305 or crypto_auth() for that, which is not
     *                      optional for security)
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_stream($len, $nonce, $key)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($len, 'int', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 3);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_STREAM_NONCEBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_SECRETBOX_NONCEBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_STREAM_KEYBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_STREAM_KEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_stream($len, $nonce, $key);
        }
        if (self::use_fallback('crypto_stream')) {
            return (string) call_user_func('\\Sodium\\crypto_stream', $len, $nonce, $key);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Core32_XSalsa20::xsalsa20($len, $nonce, $key);
        }
        return ParagonIE_Sodium_Core_XSalsa20::xsalsa20($len, $nonce, $key);
    }

    /**
     * DANGER! UNAUTHENTICATED ENCRYPTION!
     *
     * Unless you are following expert advice, do not used this feature.
     *
     * Algorithm: XSalsa20
     *
     * This DOES NOT provide ciphertext integrity.
     *
     * @param string $message Plaintext message
     * @param string $nonce Number to be used Once; must be 24 bytes
     * @param string $key Encryption key
     * @return string         Encrypted text which is vulnerable to chosen-
     *                        ciphertext attacks unless you implement some
     *                        other mitigation to the ciphertext (i.e.
     *                        Encrypt then MAC)
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_stream_xor($message, $nonce, $key)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($message, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 3);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_STREAM_NONCEBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_SECRETBOX_NONCEBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_STREAM_KEYBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_SECRETBOX_KEYBYTES long.');
        }

        if (self::useNewSodiumAPI()) {
            return sodium_crypto_stream_xor($message, $nonce, $key);
        }
        if (self::use_fallback('crypto_stream_xor')) {
            return (string) call_user_func('\\Sodium\\crypto_stream_xor', $message, $nonce, $key);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Core32_XSalsa20::xsalsa20_xor($message, $nonce, $key);
        }
        return ParagonIE_Sodium_Core_XSalsa20::xsalsa20_xor($message, $nonce, $key);
    }

    /**
     * Return a secure random key for use with crypto_stream
     *
     * @return string
     * @throws Exception
     * @throws Error
     */
    public static function crypto_stream_keygen()
    {
        return random_bytes(self::CRYPTO_STREAM_KEYBYTES);
    }


    /**
     * Expand a key and nonce into a keystream of pseudorandom bytes.
     *
     * @param int $len Number of bytes desired
     * @param string $nonce Number to be used Once; must be 24 bytes
     * @param string $key XChaCha20 key
     * @param bool $dontFallback
     * @return string       Pseudorandom stream that can be XORed with messages
     *                      to provide encryption (but not authentication; see
     *                      Poly1305 or crypto_auth() for that, which is not
     *                      optional for security)
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_stream_xchacha20($len, $nonce, $key, $dontFallback = false)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($len, 'int', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 3);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_STREAM_XCHACHA20_NONCEBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_SECRETBOX_XCHACHA20_NONCEBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_STREAM_XCHACHA20_KEYBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_STREAM_XCHACHA20_KEYBYTES long.');
        }

        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_stream_xchacha20($len, $nonce, $key);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Core32_XChaCha20::stream($len, $nonce, $key);
        }
        return ParagonIE_Sodium_Core_XChaCha20::stream($len, $nonce, $key);
    }

    /**
     * DANGER! UNAUTHENTICATED ENCRYPTION!
     *
     * Unless you are following expert advice, do not used this feature.
     *
     * Algorithm: XChaCha20
     *
     * This DOES NOT provide ciphertext integrity.
     *
     * @param string $message Plaintext message
     * @param string $nonce Number to be used Once; must be 24 bytes
     * @param string $key Encryption key
     * @return string         Encrypted text which is vulnerable to chosen-
     *                        ciphertext attacks unless you implement some
     *                        other mitigation to the ciphertext (i.e.
     *                        Encrypt then MAC)
     * @param bool $dontFallback
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function crypto_stream_xchacha20_xor($message, $nonce, $key, $dontFallback = false)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($message, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 2);
        ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 3);

        /* Input validation: */
        if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_STREAM_XCHACHA20_NONCEBYTES) {
            throw new SodiumException('Argument 2 must be CRYPTO_SECRETBOX_XCHACHA20_NONCEBYTES long.');
        }
        if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_STREAM_XCHACHA20_KEYBYTES) {
            throw new SodiumException('Argument 3 must be CRYPTO_SECRETBOX_XCHACHA20_KEYBYTES long.');
        }

        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_stream_xchacha20_xor($message, $nonce, $key);
        }
        if (PHP_INT_SIZE === 4) {
            return ParagonIE_Sodium_Core32_XChaCha20::streamXorIc($message, $nonce, $key);
        }
        return ParagonIE_Sodium_Core_XChaCha20::streamXorIc($message, $nonce, $key);
    }

    /**
     * Return a secure random key for use with crypto_stream_xchacha20
     *
     * @return string
     * @throws Exception
     * @throws Error
     */
    public static function crypto_stream_xchacha20_keygen()
    {
        return random_bytes(self::CRYPTO_STREAM_XCHACHA20_KEYBYTES);
    }

    /**
     * Cache-timing-safe implementation of hex2bin().
     *
     * @param string $string Hexadecimal string
     * @return string        Raw binary string
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress TooFewArguments
     * @psalm-suppress MixedArgument
     */
    public static function hex2bin($string)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($string, 'string', 1);

        if (self::useNewSodiumAPI()) {
            if (is_callable('sodium_hex2bin')) {
                return (string) sodium_hex2bin($string);
            }
        }
        if (self::use_fallback('hex2bin')) {
            return (string) call_user_func('\\Sodium\\hex2bin', $string);
        }
        return ParagonIE_Sodium_Core_Util::hex2bin($string);
    }

    /**
     * Increase a string (little endian)
     *
     * @param string $var
     *
     * @return void
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function increment(&$var)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($var, 'string', 1);

        if (self::useNewSodiumAPI()) {
            sodium_increment($var);
            return;
        }
        if (self::use_fallback('increment')) {
            $func = '\\Sodium\\increment';
            $func($var);
            return;
        }

        $len = ParagonIE_Sodium_Core_Util::strlen($var);
        $c = 1;
        $copy = '';
        for ($i = 0; $i < $len; ++$i) {
            $c += ParagonIE_Sodium_Core_Util::chrToInt(
                ParagonIE_Sodium_Core_Util::substr($var, $i, 1)
            );
            $copy .= ParagonIE_Sodium_Core_Util::intToChr($c);
            $c >>= 8;
        }
        $var = $copy;
    }

    /**
     * @param string $str
     * @return bool
     *
     * @throws SodiumException
     */
    public static function is_zero($str)
    {
        $d = 0;
        for ($i = 0; $i < 32; ++$i) {
            $d |= ParagonIE_Sodium_Core_Util::chrToInt($str[$i]);
        }
        return ((($d - 1) >> 31) & 1) === 1;
    }

    /**
     * The equivalent to the libsodium minor version we aim to be compatible
     * with (sans pwhash and memzero).
     *
     * @return int
     */
    public static function library_version_major()
    {
        if (self::useNewSodiumAPI() && defined('SODIUM_LIBRARY_MAJOR_VERSION')) {
            return SODIUM_LIBRARY_MAJOR_VERSION;
        }
        if (self::use_fallback('library_version_major')) {
            /** @psalm-suppress UndefinedFunction */
            return (int) call_user_func('\\Sodium\\library_version_major');
        }
        return self::LIBRARY_VERSION_MAJOR;
    }

    /**
     * The equivalent to the libsodium minor version we aim to be compatible
     * with (sans pwhash and memzero).
     *
     * @return int
     */
    public static function library_version_minor()
    {
        if (self::useNewSodiumAPI() && defined('SODIUM_LIBRARY_MINOR_VERSION')) {
            return SODIUM_LIBRARY_MINOR_VERSION;
        }
        if (self::use_fallback('library_version_minor')) {
            /** @psalm-suppress UndefinedFunction */
            return (int) call_user_func('\\Sodium\\library_version_minor');
        }
        return self::LIBRARY_VERSION_MINOR;
    }

    /**
     * Compare two strings.
     *
     * @param string $left
     * @param string $right
     * @return int
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     */
    public static function memcmp($left, $right)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($left, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($right, 'string', 2);

        if (self::useNewSodiumAPI()) {
            return sodium_memcmp($left, $right);
        }
        if (self::use_fallback('memcmp')) {
            return (int) call_user_func('\\Sodium\\memcmp', $left, $right);
        }
        /** @var string $left */
        /** @var string $right */
        return ParagonIE_Sodium_Core_Util::memcmp($left, $right);
    }

    /**
     * It's actually not possible to zero memory buffers in PHP. You need the
     * native library for that.
     *
     * @param string|null $var
     * @param-out string|null $var
     *
     * @return void
     * @throws SodiumException (Unless libsodium is installed)
     * @throws TypeError
     * @psalm-suppress TooFewArguments
     */
    public static function memzero(&$var)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($var, 'string', 1);

        if (self::useNewSodiumAPI()) {
            /** @psalm-suppress MixedArgument */
            sodium_memzero($var);
            return;
        }
        if (self::use_fallback('memzero')) {
            $func = '\\Sodium\\memzero';
            $func($var);
            if ($var === null) {
                return;
            }
        }
        // This is the best we can do.
        throw new SodiumException(
            'This is not implemented in sodium_compat, as it is not possible to securely wipe memory from PHP. ' .
            'To fix this error, make sure libsodium is installed and the PHP extension is enabled.'
        );
    }

    /**
     * @param string $unpadded
     * @param int $blockSize
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function pad($unpadded, $blockSize, $dontFallback = false)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($unpadded, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($blockSize, 'int', 2);

        $unpadded = (string) $unpadded;
        $blockSize = (int) $blockSize;

        if (self::useNewSodiumAPI() && !$dontFallback) {
            return (string) sodium_pad($unpadded, $blockSize);
        }

        if ($blockSize <= 0) {
            throw new SodiumException(
                'block size cannot be less than 1'
            );
        }
        $unpadded_len = ParagonIE_Sodium_Core_Util::strlen($unpadded);
        $xpadlen = ($blockSize - 1);
        if (($blockSize & ($blockSize - 1)) === 0) {
            $xpadlen -= $unpadded_len & ($blockSize - 1);
        } else {
            $xpadlen -= $unpadded_len % $blockSize;
        }

        $xpadded_len = $unpadded_len + $xpadlen;
        $padded = str_repeat("\0", $xpadded_len - 1);
        if ($unpadded_len > 0) {
            $st = 1;
            $i = 0;
            $k = $unpadded_len;
            for ($j = 0; $j <= $xpadded_len; ++$j) {
                $i = (int) $i;
                $k = (int) $k;
                $st = (int) $st;
                if ($j >= $unpadded_len) {
                    $padded[$j] = "\0";
                } else {
                    $padded[$j] = $unpadded[$j];
                }
                /** @var int $k */
                $k -= $st;
                $st = (int) (~(
                            (
                                (
                                    ($k >> 48)
                                        |
                                    ($k >> 32)
                                        |
                                    ($k >> 16)
                                        |
                                    $k
                                ) - 1
                            ) >> 16
                        )
                    ) & 1;
                $i += $st;
            }
        }

        $mask = 0;
        $tail = $xpadded_len;
        for ($i = 0; $i < $blockSize; ++$i) {
            # barrier_mask = (unsigned char)
            #     (((i ^ xpadlen) - 1U) >> ((sizeof(size_t) - 1U) * CHAR_BIT));
            $barrier_mask = (($i ^ $xpadlen) -1) >> ((PHP_INT_SIZE << 3) - 1);
            # tail[-i] = (tail[-i] & mask) | (0x80 & barrier_mask);
            $padded[$tail - $i] = ParagonIE_Sodium_Core_Util::intToChr(
                (ParagonIE_Sodium_Core_Util::chrToInt($padded[$tail - $i]) & $mask)
                    |
                (0x80 & $barrier_mask)
            );
            # mask |= barrier_mask;
            $mask |= $barrier_mask;
        }
        return $padded;
    }

    /**
     * @param string $padded
     * @param int $blockSize
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function unpad($padded, $blockSize, $dontFallback = false)
    {
        /* Type checks: */
        ParagonIE_Sodium_Core_Util::declareScalarType($padded, 'string', 1);
        ParagonIE_Sodium_Core_Util::declareScalarType($blockSize, 'int', 2);

        $padded = (string) $padded;
        $blockSize = (int) $blockSize;

        if (self::useNewSodiumAPI() && !$dontFallback) {
            return (string) sodium_unpad($padded, $blockSize);
        }
        if ($blockSize <= 0) {
            throw new SodiumException('block size cannot be less than 1');
        }
        $padded_len = ParagonIE_Sodium_Core_Util::strlen($padded);
        if ($padded_len < $blockSize) {
            throw new SodiumException('invalid padding');
        }

        # tail = &padded[padded_len - 1U];
        $tail = $padded_len - 1;

        $acc = 0;
        $valid = 0;
        $pad_len = 0;

        $found = 0;
        for ($i = 0; $i < $blockSize; ++$i) {
            # c = tail[-i];
            $c = ParagonIE_Sodium_Core_Util::chrToInt($padded[$tail - $i]);

            # is_barrier =
            #     (( (acc - 1U) & (pad_len - 1U) & ((c ^ 0x80) - 1U) ) >> 8) & 1U;
            $is_barrier = (
                (
                    ($acc - 1) & ($pad_len - 1) & (($c ^ 80) - 1)
                ) >> 7
            ) & 1;
            $is_barrier &= ~$found;
            $found |= $is_barrier;

            # acc |= c;
            $acc |= $c;

            # pad_len |= i & (1U + ~is_barrier);
            $pad_len |= $i & (1 + ~$is_barrier);

            # valid |= (unsigned char) is_barrier;
            $valid |= ($is_barrier & 0xff);
        }
        # unpadded_len = padded_len - 1U - pad_len;
        $unpadded_len = $padded_len - 1 - $pad_len;
        if ($valid !== 1) {
            throw new SodiumException('invalid padding');
        }
        return ParagonIE_Sodium_Core_Util::substr($padded, 0, $unpadded_len);
    }

    /**
     * Will sodium_compat run fast on the current hardware and PHP configuration?
     *
     * @return bool
     */
    public static function polyfill_is_fast()
    {
        if (extension_loaded('sodium')) {
            return true;
        }
        if (extension_loaded('libsodium')) {
            return true;
        }
        return PHP_INT_SIZE === 8;
    }

    /**
     * Generate a string of bytes from the kernel's CSPRNG.
     * Proudly uses /dev/urandom (if getrandom(2) is not available).
     *
     * @param int $numBytes
     * @return string
     * @throws Exception
     * @throws TypeError
     */
    public static function randombytes_buf($numBytes)
    {
        /* Type checks: */
        if (!is_int($numBytes)) {
            if (is_numeric($numBytes)) {
                $numBytes = (int) $numBytes;
            } else {
                throw new TypeError(
                    'Argument 1 must be an integer, ' . gettype($numBytes) . ' given.'
                );
            }
        }
        if (self::use_fallback('randombytes_buf')) {
            return (string) call_user_func('\\Sodium\\randombytes_buf', $numBytes);
        }
        return random_bytes($numBytes);
    }

    /**
     * Generate an integer between 0 and $range (non-inclusive).
     *
     * @param int $range
     * @return int
     * @throws Exception
     * @throws Error
     * @throws TypeError
     */
    public static function randombytes_uniform($range)
    {
        /* Type checks: */
        if (!is_int($range)) {
            if (is_numeric($range)) {
                $range = (int) $range;
            } else {
                throw new TypeError(
                    'Argument 1 must be an integer, ' . gettype($range) . ' given.'
                );
            }
        }
        if (self::use_fallback('randombytes_uniform')) {
            return (int) call_user_func('\\Sodium\\randombytes_uniform', $range);
        }
        return random_int(0, $range - 1);
    }

    /**
     * Generate a random 16-bit integer.
     *
     * @return int
     * @throws Exception
     * @throws Error
     * @throws TypeError
     */
    public static function randombytes_random16()
    {
        if (self::use_fallback('randombytes_random16')) {
            return (int) call_user_func('\\Sodium\\randombytes_random16');
        }
        return random_int(0, 65535);
    }

    /**
     * @param string $p
     * @param bool $dontFallback
     * @return bool
     * @throws SodiumException
     */
    public static function ristretto255_is_valid_point($p, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_is_valid_point($p);
        }
        try {
            $r = ParagonIE_Sodium_Core_Ristretto255::ristretto255_frombytes($p);
            return $r['res'] === 0 &&
                ParagonIE_Sodium_Core_Ristretto255::ristretto255_point_is_canonical($p) === 1;
        } catch (SodiumException $ex) {
            if ($ex->getMessage() === 'S is not canonical') {
                return false;
            }
            throw $ex;
        }
    }

    /**
     * @param string $p
     * @param string $q
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_add($p, $q, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_add($p, $q);
        }
        return ParagonIE_Sodium_Core_Ristretto255::ristretto255_add($p, $q);
    }

    /**
     * @param string $p
     * @param string $q
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_sub($p, $q, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_sub($p, $q);
        }
        return ParagonIE_Sodium_Core_Ristretto255::ristretto255_sub($p, $q);
    }

    /**
     * @param string $r
     * @param bool $dontFallback
     * @return string
     *
     * @throws SodiumException
     */
    public static function ristretto255_from_hash($r, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_from_hash($r);
        }
        return ParagonIE_Sodium_Core_Ristretto255::ristretto255_from_hash($r);
    }

    /**
     * @param bool $dontFallback
     * @return string
     *
     * @throws SodiumException
     */
    public static function ristretto255_random($dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_random();
        }
        return ParagonIE_Sodium_Core_Ristretto255::ristretto255_random();
    }

    /**
     * @param bool $dontFallback
     * @return string
     *
     * @throws SodiumException
     */
    public static function ristretto255_scalar_random($dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_scalar_random();
        }
        return ParagonIE_Sodium_Core_Ristretto255::ristretto255_scalar_random();
    }

    /**
     * @param string $s
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_scalar_invert($s, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_scalar_invert($s);
        }
        return ParagonIE_Sodium_Core_Ristretto255::ristretto255_scalar_invert($s);
    }
    /**
     * @param string $s
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_scalar_negate($s, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_scalar_negate($s);
        }
        return ParagonIE_Sodium_Core_Ristretto255::ristretto255_scalar_negate($s);
    }

    /**
     * @param string $s
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_scalar_complement($s, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_scalar_complement($s);
        }
        return ParagonIE_Sodium_Core_Ristretto255::ristretto255_scalar_complement($s);
    }

    /**
     * @param string $x
     * @param string $y
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_scalar_add($x, $y, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_scalar_add($x, $y);
        }
        return ParagonIE_Sodium_Core_Ristretto255::ristretto255_scalar_add($x, $y);
    }

    /**
     * @param string $x
     * @param string $y
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_scalar_sub($x, $y, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_scalar_sub($x, $y);
        }
        return ParagonIE_Sodium_Core_Ristretto255::ristretto255_scalar_sub($x, $y);
    }

    /**
     * @param string $x
     * @param string $y
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_scalar_mul($x, $y, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_scalar_mul($x, $y);
        }
        return ParagonIE_Sodium_Core_Ristretto255::ristretto255_scalar_mul($x, $y);
    }

    /**
     * @param string $n
     * @param string $p
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function scalarmult_ristretto255($n, $p, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_scalarmult_ristretto255($n, $p);
        }
        return ParagonIE_Sodium_Core_Ristretto255::scalarmult_ristretto255($n, $p);
    }

    /**
     * @param string $n
     * @param string $p
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function scalarmult_ristretto255_base($n, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_scalarmult_ristretto255_base($n);
        }
        return ParagonIE_Sodium_Core_Ristretto255::scalarmult_ristretto255_base($n);
    }

    /**
     * @param string $s
     * @param bool $dontFallback
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_scalar_reduce($s, $dontFallback = false)
    {
        if (self::useNewSodiumAPI() && !$dontFallback) {
            return sodium_crypto_core_ristretto255_scalar_reduce($s);
        }
        return ParagonIE_Sodium_Core_Ristretto255::sc_reduce($s);
    }

    /**
     * Runtime testing method for 32-bit platforms.
     *
     * Usage: If runtime_speed_test() returns FALSE, then our 32-bit
     *        implementation is to slow to use safely without risking timeouts.
     *        If this happens, install sodium from PECL to get acceptable
     *        performance.
     *
     * @param int $iterations Number of multiplications to attempt
     * @param int $maxTimeout Milliseconds
     * @return bool           TRUE if we're fast enough, FALSE is not
     * @throws SodiumException
     */
    public static function runtime_speed_test($iterations, $maxTimeout)
    {
        if (self::polyfill_is_fast()) {
            return true;
        }
        /** @var float $end */
        $end = 0.0;
        /** @var float $start */
        $start = microtime(true);
        /** @var ParagonIE_Sodium_Core32_Int64 $a */
        $a = ParagonIE_Sodium_Core32_Int64::fromInt(random_int(3, 1 << 16));
        for ($i = 0; $i < $iterations; ++$i) {
            /** @var ParagonIE_Sodium_Core32_Int64 $b */
            $b = ParagonIE_Sodium_Core32_Int64::fromInt(random_int(3, 1 << 16));
            $a->mulInt64($b);
        }
        /** @var float $end */
        $end = microtime(true);
        /** @var int $diff */
        $diff = (int) ceil(($end - $start) * 1000);
        return $diff < $maxTimeout;
    }

    /**
     * Add two numbers (little-endian unsigned), storing the value in the first
     * parameter.
     *
     * This mutates $val.
     *
     * @param string $val
     * @param string $addv
     * @return void
     * @throws SodiumException
     */
    public static function sub(&$val, $addv)
    {
        $val_len = ParagonIE_Sodium_Core_Util::strlen($val);
        $addv_len = ParagonIE_Sodium_Core_Util::strlen($addv);
        if ($val_len !== $addv_len) {
            throw new SodiumException('values must have the same length');
        }
        $A = ParagonIE_Sodium_Core_Util::stringToIntArray($val);
        $B = ParagonIE_Sodium_Core_Util::stringToIntArray($addv);

        $c = 0;
        for ($i = 0; $i < $val_len; $i++) {
            $c = ($A[$i] - $B[$i] - $c);
            $A[$i] = ($c & 0xff);
            $c = ($c >> 8) & 1;
        }
        $val = ParagonIE_Sodium_Core_Util::intArrayToString($A);
    }

    /**
     * This emulates libsodium's version_string() function, except ours is
     * prefixed with 'polyfill-'.
     *
     * @return string
     * @psalm-suppress MixedInferredReturnType
     * @psalm-suppress UndefinedFunction
     */
    public static function version_string()
    {
        if (self::useNewSodiumAPI()) {
            return (string) sodium_version_string();
        }
        if (self::use_fallback('version_string')) {
            return (string) call_user_func('\\Sodium\\version_string');
        }
        return (string) self::VERSION_STRING;
    }

    /**
     * Should we use the libsodium core function instead?
     * This is always a good idea, if it's available. (Unless we're in the
     * middle of running our unit test suite.)
     *
     * If ext/libsodium is available, use it. Return TRUE.
     * Otherwise, we have to use the code provided herein. Return FALSE.
     *
     * @param string $sodium_func_name
     *
     * @return bool
     */
    protected static function use_fallback($sodium_func_name = '')
    {
        static $res = null;
        if ($res === null) {
            $res = extension_loaded('libsodium') && PHP_VERSION_ID >= 50300;
        }
        if ($res === false) {
            // No libsodium installed
            return false;
        }
        if (self::$disableFallbackForUnitTests) {
            // Don't fallback. Use the PHP implementation.
            return false;
        }
        if (!empty($sodium_func_name)) {
            return is_callable('\\Sodium\\' . $sodium_func_name);
        }
        return true;
    }

    /**
     * Libsodium as implemented in PHP 7.2
     * and/or ext/sodium (via PECL)
     *
     * @ref https://wiki.php.net/rfc/libsodium
     * @return bool
     */
    protected static function useNewSodiumAPI()
    {
        static $res = null;
        if ($res === null) {
            $res = PHP_VERSION_ID >= 70000 && extension_loaded('sodium');
        }
        if (self::$disableFallbackForUnitTests) {
            // Don't fallback. Use the PHP implementation.
            return false;
        }
        return (bool) $res;
    }
}
Core/XSalsa20.php000064400000002533152331075510007507 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_XSalsa20', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_XSalsa20
 */
abstract class ParagonIE_Sodium_Core_XSalsa20 extends ParagonIE_Sodium_Core_HSalsa20
{
    /**
     * Expand a key and nonce into an xsalsa20 keystream.
     *
     * @internal You should not use this directly from another application
     *
     * @param int $len
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function xsalsa20($len, $nonce, $key)
    {
        $ret = self::salsa20(
            $len,
            self::substr($nonce, 16, 8),
            self::hsalsa20($nonce, $key)
        );
        return $ret;
    }

    /**
     * Encrypt a string with XSalsa20. Doesn't provide integrity.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function xsalsa20_xor($message, $nonce, $key)
    {
        return self::xorStrings(
            $message,
            self::xsalsa20(
                self::strlen($message),
                $nonce,
                $key
            )
        );
    }
}
Core/Curve25519.php000064400000446155152331075510007660 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_Curve25519', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_Curve25519
 *
 * Implements Curve25519 core functions
 *
 * Based on the ref10 curve25519 code provided by libsodium
 *
 * @ref https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_core/curve25519/ref10/curve25519_ref10.c
 */
abstract class ParagonIE_Sodium_Core_Curve25519 extends ParagonIE_Sodium_Core_Curve25519_H
{
    /**
     * Get a field element of size 10 with a value of 0
     *
     * @internal You should not use this directly from another application
     *
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public static function fe_0()
    {
        return ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(
            array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
        );
    }

    /**
     * Get a field element of size 10 with a value of 1
     *
     * @internal You should not use this directly from another application
     *
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public static function fe_1()
    {
        return ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(
            array(1, 0, 0, 0, 0, 0, 0, 0, 0, 0)
        );
    }

    /**
     * Add two field elements.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $g
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedOperand
     */
    public static function fe_add(
        ParagonIE_Sodium_Core_Curve25519_Fe $f,
        ParagonIE_Sodium_Core_Curve25519_Fe $g
    ) {
        /** @var array<int, int> $arr */
        $arr = array();
        for ($i = 0; $i < 10; ++$i) {
            $arr[$i] = (int) ($f[$i] + $g[$i]);
        }
        return ParagonIE_Sodium_Core_Curve25519_Fe::fromArray($arr);
    }

    /**
     * Constant-time conditional move.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $g
     * @param int $b
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     * @psalm-suppress MixedAssignment
     */
    public static function fe_cmov(
        ParagonIE_Sodium_Core_Curve25519_Fe $f,
        ParagonIE_Sodium_Core_Curve25519_Fe $g,
        $b = 0
    ) {
        /** @var array<int, int> $h */
        $h = array();
        $b *= -1;
        for ($i = 0; $i < 10; ++$i) {
            $x = (($f[$i] ^ $g[$i]) & $b);
            $h[$i] = ($f[$i]) ^ $x;
        }
        return ParagonIE_Sodium_Core_Curve25519_Fe::fromArray($h);
    }

    /**
     * Create a copy of a field element.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public static function fe_copy(ParagonIE_Sodium_Core_Curve25519_Fe $f)
    {
        $h = clone $f;
        return $h;
    }

    /**
     * Give: 32-byte string.
     * Receive: A field element object to use for internal calculations.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $s
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     * @throws RangeException
     * @throws TypeError
     */
    public static function fe_frombytes($s)
    {
        if (self::strlen($s) !== 32) {
            throw new RangeException('Expected a 32-byte string.');
        }
        /** @var int $h0 */
        $h0 = self::load_4($s);
        /** @var int $h1 */
        $h1 = self::load_3(self::substr($s, 4, 3)) << 6;
        /** @var int $h2 */
        $h2 = self::load_3(self::substr($s, 7, 3)) << 5;
        /** @var int $h3 */
        $h3 = self::load_3(self::substr($s, 10, 3)) << 3;
        /** @var int $h4 */
        $h4 = self::load_3(self::substr($s, 13, 3)) << 2;
        /** @var int $h5 */
        $h5 = self::load_4(self::substr($s, 16, 4));
        /** @var int $h6 */
        $h6 = self::load_3(self::substr($s, 20, 3)) << 7;
        /** @var int $h7 */
        $h7 = self::load_3(self::substr($s, 23, 3)) << 5;
        /** @var int $h8 */
        $h8 = self::load_3(self::substr($s, 26, 3)) << 4;
        /** @var int $h9 */
        $h9 = (self::load_3(self::substr($s, 29, 3)) & 8388607) << 2;

        /** @var int $carry9 */
        $carry9 = ($h9 + (1 << 24)) >> 25;
        $h0 += self::mul($carry9, 19, 5);
        $h9 -= $carry9 << 25;
        /** @var int $carry1 */
        $carry1 = ($h1 + (1 << 24)) >> 25;
        $h2 += $carry1;
        $h1 -= $carry1 << 25;
        /** @var int $carry3 */
        $carry3 = ($h3 + (1 << 24)) >> 25;
        $h4 += $carry3;
        $h3 -= $carry3 << 25;
        /** @var int $carry5 */
        $carry5 = ($h5 + (1 << 24)) >> 25;
        $h6 += $carry5;
        $h5 -= $carry5 << 25;
        /** @var int $carry7 */
        $carry7 = ($h7 + (1 << 24)) >> 25;
        $h8 += $carry7;
        $h7 -= $carry7 << 25;

        /** @var int $carry0 */
        $carry0 = ($h0 + (1 << 25)) >> 26;
        $h1 += $carry0;
        $h0 -= $carry0 << 26;
        /** @var int $carry2 */
        $carry2 = ($h2 + (1 << 25)) >> 26;
        $h3 += $carry2;
        $h2 -= $carry2 << 26;
        /** @var int $carry4 */
        $carry4 = ($h4 + (1 << 25)) >> 26;
        $h5 += $carry4;
        $h4 -= $carry4 << 26;
        /** @var int $carry6 */
        $carry6 = ($h6 + (1 << 25)) >> 26;
        $h7 += $carry6;
        $h6 -= $carry6 << 26;
        /** @var int $carry8 */
        $carry8 = ($h8 + (1 << 25)) >> 26;
        $h9 += $carry8;
        $h8 -= $carry8 << 26;

        return ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(
            array(
                (int) $h0,
                (int) $h1,
                (int) $h2,
                (int) $h3,
                (int) $h4,
                (int) $h5,
                (int) $h6,
                (int) $h7,
                (int) $h8,
                (int) $h9
            )
        );
    }

    /**
     * Convert a field element to a byte string.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $h
     * @return string
     */
    public static function fe_tobytes(ParagonIE_Sodium_Core_Curve25519_Fe $h)
    {
        /** @var int $h0 */
        $h0 = (int) $h[0];
        /** @var int $h1 */
        $h1 = (int) $h[1];
        /** @var int $h2 */
        $h2 = (int) $h[2];
        /** @var int $h3 */
        $h3 = (int) $h[3];
        /** @var int $h4 */
        $h4 = (int) $h[4];
        /** @var int $h5 */
        $h5 = (int) $h[5];
        /** @var int $h6 */
        $h6 = (int) $h[6];
        /** @var int $h7 */
        $h7 = (int) $h[7];
        /** @var int $h8 */
        $h8 = (int) $h[8];
        /** @var int $h9 */
        $h9 = (int) $h[9];

        /** @var int $q */
        $q = (self::mul($h9, 19, 5) + (1 << 24)) >> 25;
        /** @var int $q */
        $q = ($h0 + $q) >> 26;
        /** @var int $q */
        $q = ($h1 + $q) >> 25;
        /** @var int $q */
        $q = ($h2 + $q) >> 26;
        /** @var int $q */
        $q = ($h3 + $q) >> 25;
        /** @var int $q */
        $q = ($h4 + $q) >> 26;
        /** @var int $q */
        $q = ($h5 + $q) >> 25;
        /** @var int $q */
        $q = ($h6 + $q) >> 26;
        /** @var int $q */
        $q = ($h7 + $q) >> 25;
        /** @var int $q */
        $q = ($h8 + $q) >> 26;
        /** @var int $q */
        $q = ($h9 + $q) >> 25;

        $h0 += self::mul($q, 19, 5);

        /** @var int $carry0 */
        $carry0 = $h0 >> 26;
        $h1 += $carry0;
        $h0 -= $carry0 << 26;
        /** @var int $carry1 */
        $carry1 = $h1 >> 25;
        $h2 += $carry1;
        $h1 -= $carry1 << 25;
        /** @var int $carry2 */
        $carry2 = $h2 >> 26;
        $h3 += $carry2;
        $h2 -= $carry2 << 26;
        /** @var int $carry3 */
        $carry3 = $h3 >> 25;
        $h4 += $carry3;
        $h3 -= $carry3 << 25;
        /** @var int $carry4 */
        $carry4 = $h4 >> 26;
        $h5 += $carry4;
        $h4 -= $carry4 << 26;
        /** @var int $carry5 */
        $carry5 = $h5 >> 25;
        $h6 += $carry5;
        $h5 -= $carry5 << 25;
        /** @var int $carry6 */
        $carry6 = $h6 >> 26;
        $h7 += $carry6;
        $h6 -= $carry6 << 26;
        /** @var int $carry7 */
        $carry7 = $h7 >> 25;
        $h8 += $carry7;
        $h7 -= $carry7 << 25;
        /** @var int $carry8 */
        $carry8 = $h8 >> 26;
        $h9 += $carry8;
        $h8 -= $carry8 << 26;
        /** @var int $carry9 */
        $carry9 = $h9 >> 25;
        $h9 -= $carry9 << 25;

        /**
         * @var array<int, int>
         */
        $s = array(
            (int) (($h0 >> 0) & 0xff),
            (int) (($h0 >> 8) & 0xff),
            (int) (($h0 >> 16) & 0xff),
            (int) ((($h0 >> 24) | ($h1 << 2)) & 0xff),
            (int) (($h1 >> 6) & 0xff),
            (int) (($h1 >> 14) & 0xff),
            (int) ((($h1 >> 22) | ($h2 << 3)) & 0xff),
            (int) (($h2 >> 5) & 0xff),
            (int) (($h2 >> 13) & 0xff),
            (int) ((($h2 >> 21) | ($h3 << 5)) & 0xff),
            (int) (($h3 >> 3) & 0xff),
            (int) (($h3 >> 11) & 0xff),
            (int) ((($h3 >> 19) | ($h4 << 6)) & 0xff),
            (int) (($h4 >> 2) & 0xff),
            (int) (($h4 >> 10) & 0xff),
            (int) (($h4 >> 18) & 0xff),
            (int) (($h5 >> 0) & 0xff),
            (int) (($h5 >> 8) & 0xff),
            (int) (($h5 >> 16) & 0xff),
            (int) ((($h5 >> 24) | ($h6 << 1)) & 0xff),
            (int) (($h6 >> 7) & 0xff),
            (int) (($h6 >> 15) & 0xff),
            (int) ((($h6 >> 23) | ($h7 << 3)) & 0xff),
            (int) (($h7 >> 5) & 0xff),
            (int) (($h7 >> 13) & 0xff),
            (int) ((($h7 >> 21) | ($h8 << 4)) & 0xff),
            (int) (($h8 >> 4) & 0xff),
            (int) (($h8 >> 12) & 0xff),
            (int) ((($h8 >> 20) | ($h9 << 6)) & 0xff),
            (int) (($h9 >> 2) & 0xff),
            (int) (($h9 >> 10) & 0xff),
            (int) (($h9 >> 18) & 0xff)
        );
        return self::intArrayToString($s);
    }

    /**
     * Is a field element negative? (1 = yes, 0 = no. Used in calculations.)
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @return int
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fe_isnegative(ParagonIE_Sodium_Core_Curve25519_Fe $f)
    {
        $str = self::fe_tobytes($f);
        return (int) (self::chrToInt($str[0]) & 1);
    }

    /**
     * Returns 0 if this field element results in all NUL bytes.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fe_isnonzero(ParagonIE_Sodium_Core_Curve25519_Fe $f)
    {
        static $zero;
        if ($zero === null) {
            $zero = str_repeat("\x00", 32);
        }
        /** @var string $zero */
        /** @var string $str */
        $str = self::fe_tobytes($f);
        return !self::verify_32($str, (string) $zero);
    }

    /**
     * Multiply two field elements
     *
     * h = f * g
     *
     * @internal You should not use this directly from another application
     *
     * @security Is multiplication a source of timing leaks? If so, can we do
     *           anything to prevent that from happening?
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $g
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public static function fe_mul(
        ParagonIE_Sodium_Core_Curve25519_Fe $f,
        ParagonIE_Sodium_Core_Curve25519_Fe $g
    ) {
        /** @var int $f0 */
        $f0 = $f[0];
        /** @var int $f1 */
        $f1 = $f[1];
        /** @var int $f2 */
        $f2 = $f[2];
        /** @var int $f3 */
        $f3 = $f[3];
        /** @var int $f4 */
        $f4 = $f[4];
        /** @var int $f5 */
        $f5 = $f[5];
        /** @var int $f6 */
        $f6 = $f[6];
        /** @var int $f7 */
        $f7 = $f[7];
        /** @var int $f8 */
        $f8 = $f[8];
        /** @var int $f9 */
        $f9 = $f[9];
        /** @var int $g0 */
        $g0 = $g[0];
        /** @var int $g1 */
        $g1 = $g[1];
        /** @var int $g2 */
        $g2 = $g[2];
        /** @var int $g3 */
        $g3 = $g[3];
        /** @var int $g4 */
        $g4 = $g[4];
        /** @var int $g5 */
        $g5 = $g[5];
        /** @var int $g6 */
        $g6 = $g[6];
        /** @var int $g7 */
        $g7 = $g[7];
        /** @var int $g8 */
        $g8 = $g[8];
        /** @var int $g9 */
        $g9 = $g[9];
        $g1_19 = self::mul($g1, 19, 5);
        $g2_19 = self::mul($g2, 19, 5);
        $g3_19 = self::mul($g3, 19, 5);
        $g4_19 = self::mul($g4, 19, 5);
        $g5_19 = self::mul($g5, 19, 5);
        $g6_19 = self::mul($g6, 19, 5);
        $g7_19 = self::mul($g7, 19, 5);
        $g8_19 = self::mul($g8, 19, 5);
        $g9_19 = self::mul($g9, 19, 5);
        /** @var int $f1_2 */
        $f1_2 = $f1 << 1;
        /** @var int $f3_2 */
        $f3_2 = $f3 << 1;
        /** @var int $f5_2 */
        $f5_2 = $f5 << 1;
        /** @var int $f7_2 */
        $f7_2 = $f7 << 1;
        /** @var int $f9_2 */
        $f9_2 = $f9 << 1;
        $f0g0    = self::mul($f0,    $g0, 26);
        $f0g1    = self::mul($f0,    $g1, 25);
        $f0g2    = self::mul($f0,    $g2, 26);
        $f0g3    = self::mul($f0,    $g3, 25);
        $f0g4    = self::mul($f0,    $g4, 26);
        $f0g5    = self::mul($f0,    $g5, 25);
        $f0g6    = self::mul($f0,    $g6, 26);
        $f0g7    = self::mul($f0,    $g7, 25);
        $f0g8    = self::mul($f0,    $g8, 26);
        $f0g9    = self::mul($f0,    $g9, 26);
        $f1g0    = self::mul($f1,    $g0, 26);
        $f1g1_2  = self::mul($f1_2,  $g1, 25);
        $f1g2    = self::mul($f1,    $g2, 26);
        $f1g3_2  = self::mul($f1_2,  $g3, 25);
        $f1g4    = self::mul($f1,    $g4, 26);
        $f1g5_2  = self::mul($f1_2,  $g5, 25);
        $f1g6    = self::mul($f1,    $g6, 26);
        $f1g7_2  = self::mul($f1_2,  $g7, 25);
        $f1g8    = self::mul($f1,    $g8, 26);
        $f1g9_38 = self::mul($g9_19, $f1_2, 26);
        $f2g0    = self::mul($f2,    $g0, 26);
        $f2g1    = self::mul($f2,    $g1, 25);
        $f2g2    = self::mul($f2,    $g2, 26);
        $f2g3    = self::mul($f2,    $g3, 25);
        $f2g4    = self::mul($f2,    $g4, 26);
        $f2g5    = self::mul($f2,    $g5, 25);
        $f2g6    = self::mul($f2,    $g6, 26);
        $f2g7    = self::mul($f2,    $g7, 25);
        $f2g8_19 = self::mul($g8_19, $f2, 26);
        $f2g9_19 = self::mul($g9_19, $f2, 26);
        $f3g0    = self::mul($f3,    $g0, 26);
        $f3g1_2  = self::mul($f3_2,  $g1, 25);
        $f3g2    = self::mul($f3,    $g2, 26);
        $f3g3_2  = self::mul($f3_2,  $g3, 25);
        $f3g4    = self::mul($f3,    $g4, 26);
        $f3g5_2  = self::mul($f3_2,  $g5, 25);
        $f3g6    = self::mul($f3,    $g6, 26);
        $f3g7_38 = self::mul($g7_19, $f3_2, 26);
        $f3g8_19 = self::mul($g8_19, $f3, 25);
        $f3g9_38 = self::mul($g9_19, $f3_2, 26);
        $f4g0    = self::mul($f4,    $g0, 26);
        $f4g1    = self::mul($f4,    $g1, 25);
        $f4g2    = self::mul($f4,    $g2, 26);
        $f4g3    = self::mul($f4,    $g3, 25);
        $f4g4    = self::mul($f4,    $g4, 26);
        $f4g5    = self::mul($f4,    $g5, 25);
        $f4g6_19 = self::mul($g6_19, $f4, 26);
        $f4g7_19 = self::mul($g7_19, $f4, 26);
        $f4g8_19 = self::mul($g8_19, $f4, 26);
        $f4g9_19 = self::mul($g9_19, $f4, 26);
        $f5g0    = self::mul($f5,    $g0, 26);
        $f5g1_2  = self::mul($f5_2,  $g1, 25);
        $f5g2    = self::mul($f5,    $g2, 26);
        $f5g3_2  = self::mul($f5_2,  $g3, 25);
        $f5g4    = self::mul($f5,    $g4, 26);
        $f5g5_38 = self::mul($g5_19, $f5_2, 26);
        $f5g6_19 = self::mul($g6_19, $f5, 25);
        $f5g7_38 = self::mul($g7_19, $f5_2, 26);
        $f5g8_19 = self::mul($g8_19, $f5, 25);
        $f5g9_38 = self::mul($g9_19, $f5_2, 26);
        $f6g0    = self::mul($f6,    $g0, 26);
        $f6g1    = self::mul($f6,    $g1, 25);
        $f6g2    = self::mul($f6,    $g2, 26);
        $f6g3    = self::mul($f6,    $g3, 25);
        $f6g4_19 = self::mul($g4_19, $f6, 26);
        $f6g5_19 = self::mul($g5_19, $f6, 26);
        $f6g6_19 = self::mul($g6_19, $f6, 26);
        $f6g7_19 = self::mul($g7_19, $f6, 26);
        $f6g8_19 = self::mul($g8_19, $f6, 26);
        $f6g9_19 = self::mul($g9_19, $f6, 26);
        $f7g0    = self::mul($f7,    $g0, 26);
        $f7g1_2  = self::mul($f7_2,  $g1, 25);
        $f7g2    = self::mul($f7,    $g2, 26);
        $f7g3_38 = self::mul($g3_19, $f7_2, 26);
        $f7g4_19 = self::mul($g4_19, $f7, 26);
        $f7g5_38 = self::mul($g5_19, $f7_2, 26);
        $f7g6_19 = self::mul($g6_19, $f7, 25);
        $f7g7_38 = self::mul($g7_19, $f7_2, 26);
        $f7g8_19 = self::mul($g8_19, $f7, 25);
        $f7g9_38 = self::mul($g9_19,$f7_2, 26);
        $f8g0    = self::mul($f8,    $g0, 26);
        $f8g1    = self::mul($f8,    $g1, 25);
        $f8g2_19 = self::mul($g2_19, $f8, 26);
        $f8g3_19 = self::mul($g3_19, $f8, 26);
        $f8g4_19 = self::mul($g4_19, $f8, 26);
        $f8g5_19 = self::mul($g5_19, $f8, 26);
        $f8g6_19 = self::mul($g6_19, $f8, 26);
        $f8g7_19 = self::mul($g7_19, $f8, 26);
        $f8g8_19 = self::mul($g8_19, $f8, 26);
        $f8g9_19 = self::mul($g9_19, $f8, 26);
        $f9g0    = self::mul($f9,    $g0, 26);
        $f9g1_38 = self::mul($g1_19, $f9_2, 26);
        $f9g2_19 = self::mul($g2_19, $f9, 25);
        $f9g3_38 = self::mul($g3_19, $f9_2, 26);
        $f9g4_19 = self::mul($g4_19, $f9, 25);
        $f9g5_38 = self::mul($g5_19, $f9_2, 26);
        $f9g6_19 = self::mul($g6_19, $f9, 25);
        $f9g7_38 = self::mul($g7_19, $f9_2, 26);
        $f9g8_19 = self::mul($g8_19, $f9, 25);
        $f9g9_38 = self::mul($g9_19, $f9_2, 26);
        $h0 = $f0g0 + $f1g9_38 + $f2g8_19 + $f3g7_38 + $f4g6_19 + $f5g5_38 + $f6g4_19 + $f7g3_38 + $f8g2_19 + $f9g1_38;
        $h1 = $f0g1 + $f1g0    + $f2g9_19 + $f3g8_19 + $f4g7_19 + $f5g6_19 + $f6g5_19 + $f7g4_19 + $f8g3_19 + $f9g2_19;
        $h2 = $f0g2 + $f1g1_2  + $f2g0    + $f3g9_38 + $f4g8_19 + $f5g7_38 + $f6g6_19 + $f7g5_38 + $f8g4_19 + $f9g3_38;
        $h3 = $f0g3 + $f1g2    + $f2g1    + $f3g0    + $f4g9_19 + $f5g8_19 + $f6g7_19 + $f7g6_19 + $f8g5_19 + $f9g4_19;
        $h4 = $f0g4 + $f1g3_2  + $f2g2    + $f3g1_2  + $f4g0    + $f5g9_38 + $f6g8_19 + $f7g7_38 + $f8g6_19 + $f9g5_38;
        $h5 = $f0g5 + $f1g4    + $f2g3    + $f3g2    + $f4g1    + $f5g0    + $f6g9_19 + $f7g8_19 + $f8g7_19 + $f9g6_19;
        $h6 = $f0g6 + $f1g5_2  + $f2g4    + $f3g3_2  + $f4g2    + $f5g1_2  + $f6g0    + $f7g9_38 + $f8g8_19 + $f9g7_38;
        $h7 = $f0g7 + $f1g6    + $f2g5    + $f3g4    + $f4g3    + $f5g2    + $f6g1    + $f7g0    + $f8g9_19 + $f9g8_19;
        $h8 = $f0g8 + $f1g7_2  + $f2g6    + $f3g5_2  + $f4g4    + $f5g3_2  + $f6g2    + $f7g1_2  + $f8g0    + $f9g9_38;
        $h9 = $f0g9 + $f1g8    + $f2g7    + $f3g6    + $f4g5    + $f5g4    + $f6g3    + $f7g2    + $f8g1    + $f9g0   ;

        /** @var int $carry0 */
        $carry0 = ($h0 + (1 << 25)) >> 26;
        $h1 += $carry0;
        $h0 -= $carry0 << 26;
        /** @var int $carry4 */
        $carry4 = ($h4 + (1 << 25)) >> 26;
        $h5 += $carry4;
        $h4 -= $carry4 << 26;

        /** @var int $carry1 */
        $carry1 = ($h1 + (1 << 24)) >> 25;
        $h2 += $carry1;
        $h1 -= $carry1 << 25;
        /** @var int $carry5 */
        $carry5 = ($h5 + (1 << 24)) >> 25;
        $h6 += $carry5;
        $h5 -= $carry5 << 25;

        /** @var int $carry2 */
        $carry2 = ($h2 + (1 << 25)) >> 26;
        $h3 += $carry2;
        $h2 -= $carry2 << 26;
        /** @var int $carry6 */
        $carry6 = ($h6 + (1 << 25)) >> 26;
        $h7 += $carry6;
        $h6 -= $carry6 << 26;

        /** @var int $carry3 */
        $carry3 = ($h3 + (1 << 24)) >> 25;
        $h4 += $carry3;
        $h3 -= $carry3 << 25;
        /** @var int $carry7 */
        $carry7 = ($h7 + (1 << 24)) >> 25;
        $h8 += $carry7;
        $h7 -= $carry7 << 25;

        /** @var int $carry4 */
        $carry4 = ($h4 + (1 << 25)) >> 26;
        $h5 += $carry4;
        $h4 -= $carry4 << 26;
        /** @var int $carry8 */
        $carry8 = ($h8 + (1 << 25)) >> 26;
        $h9 += $carry8;
        $h8 -= $carry8 << 26;

        /** @var int $carry9 */
        $carry9 = ($h9 + (1 << 24)) >> 25;
        $h0 += self::mul($carry9, 19, 5);
        $h9 -= $carry9 << 25;

        /** @var int $carry0 */
        $carry0 = ($h0 + (1 << 25)) >> 26;
        $h1 += $carry0;
        $h0 -= $carry0 << 26;

        return ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(
            array(
                (int) $h0,
                (int) $h1,
                (int) $h2,
                (int) $h3,
                (int) $h4,
                (int) $h5,
                (int) $h6,
                (int) $h7,
                (int) $h8,
                (int) $h9
            )
        );
    }

    /**
     * Get the negative values for each piece of the field element.
     *
     * h = -f
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     * @psalm-suppress MixedAssignment
     */
    public static function fe_neg(ParagonIE_Sodium_Core_Curve25519_Fe $f)
    {
        $h = new ParagonIE_Sodium_Core_Curve25519_Fe();
        for ($i = 0; $i < 10; ++$i) {
            $h[$i] = -$f[$i];
        }
        return $h;
    }

    /**
     * Square a field element
     *
     * h = f * f
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public static function fe_sq(ParagonIE_Sodium_Core_Curve25519_Fe $f)
    {
        $f0 = (int) $f[0];
        $f1 = (int) $f[1];
        $f2 = (int) $f[2];
        $f3 = (int) $f[3];
        $f4 = (int) $f[4];
        $f5 = (int) $f[5];
        $f6 = (int) $f[6];
        $f7 = (int) $f[7];
        $f8 = (int) $f[8];
        $f9 = (int) $f[9];

        /** @var int $f0_2 */
        $f0_2 = $f0 << 1;
        /** @var int $f1_2 */
        $f1_2 = $f1 << 1;
        /** @var int $f2_2 */
        $f2_2 = $f2 << 1;
        /** @var int $f3_2 */
        $f3_2 = $f3 << 1;
        /** @var int $f4_2 */
        $f4_2 = $f4 << 1;
        /** @var int $f5_2 */
        $f5_2 = $f5 << 1;
        /** @var int $f6_2 */
        $f6_2 = $f6 << 1;
        /** @var int $f7_2 */
        $f7_2 = $f7 << 1;
        $f5_38 = self::mul($f5, 38, 6);
        $f6_19 = self::mul($f6, 19, 5);
        $f7_38 = self::mul($f7, 38, 6);
        $f8_19 = self::mul($f8, 19, 5);
        $f9_38 = self::mul($f9, 38, 6);
        $f0f0    = self::mul($f0,    $f0,    25);
        $f0f1_2  = self::mul($f0_2,  $f1,    24);
        $f0f2_2  = self::mul($f0_2,  $f2,    26);
        $f0f3_2  = self::mul($f0_2,  $f3,    24);
        $f0f4_2  = self::mul($f0_2,  $f4,    25);
        $f0f5_2  = self::mul($f0_2,  $f5,    25);
        $f0f6_2  = self::mul($f0_2,  $f6,    25);
        $f0f7_2  = self::mul($f0_2,  $f7,    24);
        $f0f8_2  = self::mul($f0_2,  $f8,    25);
        $f0f9_2  = self::mul($f0_2,  $f9,    25);
        $f1f1_2  = self::mul($f1_2,  $f1,    24);
        $f1f2_2  = self::mul($f1_2,  $f2,    26);
        $f1f3_4  = self::mul($f1_2,  $f3_2,  25);
        $f1f4_2  = self::mul($f1_2,  $f4,    25);
        $f1f5_4  = self::mul($f1_2,  $f5_2,  26);
        $f1f6_2  = self::mul($f1_2,  $f6,    25);
        $f1f7_4  = self::mul($f1_2,  $f7_2,  25);
        $f1f8_2  = self::mul($f1_2,  $f8,    25);
        $f1f9_76 = self::mul($f9_38, $f1_2,  25);
        $f2f2    = self::mul($f2,    $f2,    26);
        $f2f3_2  = self::mul($f2_2,  $f3,    24);
        $f2f4_2  = self::mul($f2_2,  $f4,    25);
        $f2f5_2  = self::mul($f2_2,  $f5,    25);
        $f2f6_2  = self::mul($f2_2,  $f6,    25);
        $f2f7_2  = self::mul($f2_2,  $f7,    25);
        $f2f8_38 = self::mul($f8_19, $f2_2,  27);
        $f2f9_38 = self::mul($f9_38, $f2,    26);
        $f3f3_2  = self::mul($f3_2,  $f3,    25);
        $f3f4_2  = self::mul($f3_2,  $f4,    25);
        $f3f5_4  = self::mul($f3_2,  $f5_2,  26);
        $f3f6_2  = self::mul($f3_2,  $f6,    25);
        $f3f7_76 = self::mul($f7_38, $f3_2,  25);
        $f3f8_38 = self::mul($f8_19, $f3_2,  25);
        $f3f9_76 = self::mul($f9_38, $f3_2,  25);
        $f4f4    = self::mul($f4,    $f4,    25);
        $f4f5_2  = self::mul($f4_2,  $f5,    25);
        $f4f6_38 = self::mul($f6_19, $f4_2,  26);
        $f4f7_38 = self::mul($f7_38, $f4,    25);
        $f4f8_38 = self::mul($f8_19, $f4_2,  26);
        $f4f9_38 = self::mul($f9_38, $f4,    25);
        $f5f5_38 = self::mul($f5_38, $f5,    25);
        $f5f6_38 = self::mul($f6_19, $f5_2,  26);
        $f5f7_76 = self::mul($f7_38, $f5_2,  26);
        $f5f8_38 = self::mul($f8_19, $f5_2,  26);
        $f5f9_76 = self::mul($f9_38, $f5_2,  26);
        $f6f6_19 = self::mul($f6_19, $f6,    25);
        $f6f7_38 = self::mul($f7_38, $f6,    25);
        $f6f8_38 = self::mul($f8_19, $f6_2,  26);
        $f6f9_38 = self::mul($f9_38, $f6,    25);
        $f7f7_38 = self::mul($f7_38, $f7,    24);
        $f7f8_38 = self::mul($f8_19, $f7_2,  25);
        $f7f9_76 = self::mul($f9_38, $f7_2,  25);
        $f8f8_19 = self::mul($f8_19, $f8,    25);
        $f8f9_38 = self::mul($f9_38, $f8,    25);
        $f9f9_38 = self::mul($f9_38, $f9,    25);
        $h0 = $f0f0   + $f1f9_76 + $f2f8_38 + $f3f7_76 + $f4f6_38 + $f5f5_38;
        $h1 = $f0f1_2 + $f2f9_38 + $f3f8_38 + $f4f7_38 + $f5f6_38;
        $h2 = $f0f2_2 + $f1f1_2  + $f3f9_76 + $f4f8_38 + $f5f7_76 + $f6f6_19;
        $h3 = $f0f3_2 + $f1f2_2  + $f4f9_38 + $f5f8_38 + $f6f7_38;
        $h4 = $f0f4_2 + $f1f3_4  + $f2f2    + $f5f9_76 + $f6f8_38 + $f7f7_38;
        $h5 = $f0f5_2 + $f1f4_2  + $f2f3_2  + $f6f9_38 + $f7f8_38;
        $h6 = $f0f6_2 + $f1f5_4  + $f2f4_2  + $f3f3_2  + $f7f9_76 + $f8f8_19;
        $h7 = $f0f7_2 + $f1f6_2  + $f2f5_2  + $f3f4_2  + $f8f9_38;
        $h8 = $f0f8_2 + $f1f7_4  + $f2f6_2  + $f3f5_4  + $f4f4    + $f9f9_38;
        $h9 = $f0f9_2 + $f1f8_2  + $f2f7_2  + $f3f6_2  + $f4f5_2;

        /** @var int $carry0 */
        $carry0 = ($h0 + (1 << 25)) >> 26;
        $h1 += $carry0;
        $h0 -= $carry0 << 26;
        /** @var int $carry4 */
        $carry4 = ($h4 + (1 << 25)) >> 26;
        $h5 += $carry4;
        $h4 -= $carry4 << 26;

        /** @var int $carry1 */
        $carry1 = ($h1 + (1 << 24)) >> 25;
        $h2 += $carry1;
        $h1 -= $carry1 << 25;
        /** @var int $carry5 */
        $carry5 = ($h5 + (1 << 24)) >> 25;
        $h6 += $carry5;
        $h5 -= $carry5 << 25;

        /** @var int $carry2 */
        $carry2 = ($h2 + (1 << 25)) >> 26;
        $h3 += $carry2;
        $h2 -= $carry2 << 26;
        /** @var int $carry6 */
        $carry6 = ($h6 + (1 << 25)) >> 26;
        $h7 += $carry6;
        $h6 -= $carry6 << 26;

        /** @var int $carry3 */
        $carry3 = ($h3 + (1 << 24)) >> 25;
        $h4 += $carry3;
        $h3 -= $carry3 << 25;
        /** @var int $carry7 */
        $carry7 = ($h7 + (1 << 24)) >> 25;
        $h8 += $carry7;
        $h7 -= $carry7 << 25;

        /** @var int $carry4 */
        $carry4 = ($h4 + (1 << 25)) >> 26;
        $h5 += $carry4;
        $h4 -= $carry4 << 26;
        /** @var int $carry8 */
        $carry8 = ($h8 + (1 << 25)) >> 26;
        $h9 += $carry8;
        $h8 -= $carry8 << 26;

        /** @var int $carry9 */
        $carry9 = ($h9 + (1 << 24)) >> 25;
        $h0 += self::mul($carry9, 19, 5);
        $h9 -= $carry9 << 25;

        /** @var int $carry0 */
        $carry0 = ($h0 + (1 << 25)) >> 26;
        $h1 += $carry0;
        $h0 -= $carry0 << 26;

        return ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(
            array(
                (int) $h0,
                (int) $h1,
                (int) $h2,
                (int) $h3,
                (int) $h4,
                (int) $h5,
                (int) $h6,
                (int) $h7,
                (int) $h8,
                (int) $h9
            )
        );
    }


    /**
     * Square and double a field element
     *
     * h = 2 * f * f
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public static function fe_sq2(ParagonIE_Sodium_Core_Curve25519_Fe $f)
    {
        $f0 = (int) $f[0];
        $f1 = (int) $f[1];
        $f2 = (int) $f[2];
        $f3 = (int) $f[3];
        $f4 = (int) $f[4];
        $f5 = (int) $f[5];
        $f6 = (int) $f[6];
        $f7 = (int) $f[7];
        $f8 = (int) $f[8];
        $f9 = (int) $f[9];

        /** @var int $f0_2 */
        $f0_2 = $f0 << 1;
        /** @var int $f1_2 */
        $f1_2 = $f1 << 1;
        /** @var int $f2_2 */
        $f2_2 = $f2 << 1;
        /** @var int $f3_2 */
        $f3_2 = $f3 << 1;
        /** @var int $f4_2 */
        $f4_2 = $f4 << 1;
        /** @var int $f5_2 */
        $f5_2 = $f5 << 1;
        /** @var int $f6_2 */
        $f6_2 = $f6 << 1;
        /** @var int $f7_2 */
        $f7_2 = $f7 << 1;
        $f5_38 = self::mul($f5, 38, 6); /* 1.959375*2^30 */
        $f6_19 = self::mul($f6, 19, 5); /* 1.959375*2^30 */
        $f7_38 = self::mul($f7, 38, 6); /* 1.959375*2^30 */
        $f8_19 = self::mul($f8, 19, 5); /* 1.959375*2^30 */
        $f9_38 = self::mul($f9, 38, 6); /* 1.959375*2^30 */
        $f0f0 = self::mul($f0, $f0, 24);
        $f0f1_2 = self::mul($f0_2, $f1, 24);
        $f0f2_2 = self::mul($f0_2, $f2, 24);
        $f0f3_2 = self::mul($f0_2, $f3, 24);
        $f0f4_2 = self::mul($f0_2, $f4, 24);
        $f0f5_2 = self::mul($f0_2, $f5, 24);
        $f0f6_2 = self::mul($f0_2, $f6, 24);
        $f0f7_2 = self::mul($f0_2, $f7, 24);
        $f0f8_2 = self::mul($f0_2, $f8, 24);
        $f0f9_2 = self::mul($f0_2, $f9, 24);
        $f1f1_2 = self::mul($f1_2,  $f1, 24);
        $f1f2_2 = self::mul($f1_2,  $f2, 24);
        $f1f3_4 = self::mul($f1_2,  $f3_2, 24);
        $f1f4_2 = self::mul($f1_2,  $f4, 24);
        $f1f5_4 = self::mul($f1_2,  $f5_2, 24);
        $f1f6_2 = self::mul($f1_2,  $f6, 24);
        $f1f7_4 = self::mul($f1_2,  $f7_2, 24);
        $f1f8_2 = self::mul($f1_2,  $f8, 24);
        $f1f9_76 = self::mul($f9_38, $f1_2, 24);
        $f2f2 = self::mul($f2,  $f2, 24);
        $f2f3_2 = self::mul($f2_2,  $f3, 24);
        $f2f4_2 = self::mul($f2_2,  $f4, 24);
        $f2f5_2 = self::mul($f2_2,  $f5, 24);
        $f2f6_2 = self::mul($f2_2,  $f6, 24);
        $f2f7_2 = self::mul($f2_2,  $f7, 24);
        $f2f8_38 = self::mul($f8_19, $f2_2, 25);
        $f2f9_38 = self::mul($f9_38, $f2, 24);
        $f3f3_2 = self::mul($f3_2,  $f3, 24);
        $f3f4_2 = self::mul($f3_2,  $f4, 24);
        $f3f5_4 = self::mul($f3_2,  $f5_2, 24);
        $f3f6_2 = self::mul($f3_2,  $f6, 24);
        $f3f7_76 = self::mul($f7_38, $f3_2, 24);
        $f3f8_38 = self::mul($f8_19, $f3_2, 24);
        $f3f9_76 = self::mul($f9_38, $f3_2, 24);
        $f4f4 = self::mul($f4,  $f4, 24);
        $f4f5_2 = self::mul($f4_2,  $f5, 24);
        $f4f6_38 = self::mul($f6_19, $f4_2, 25);
        $f4f7_38 = self::mul($f7_38, $f4, 24);
        $f4f8_38 = self::mul($f8_19, $f4_2, 25);
        $f4f9_38 = self::mul($f9_38, $f4, 24);
        $f5f5_38 = self::mul($f5_38, $f5, 24);
        $f5f6_38 = self::mul($f6_19, $f5_2, 24);
        $f5f7_76 = self::mul($f7_38, $f5_2, 24);
        $f5f8_38 = self::mul($f8_19, $f5_2, 24);
        $f5f9_76 = self::mul($f9_38, $f5_2, 24);
        $f6f6_19 = self::mul($f6_19, $f6, 24);
        $f6f7_38 = self::mul($f7_38, $f6, 24);
        $f6f8_38 = self::mul($f8_19, $f6_2, 25);
        $f6f9_38 = self::mul($f9_38, $f6, 24);
        $f7f7_38 = self::mul($f7_38, $f7, 24);
        $f7f8_38 = self::mul($f8_19, $f7_2, 24);
        $f7f9_76 = self::mul($f9_38, $f7_2, 24);
        $f8f8_19 = self::mul($f8_19, $f8, 24);
        $f8f9_38 = self::mul($f9_38, $f8, 24);
        $f9f9_38 = self::mul($f9_38, $f9, 24);

        /** @var int $h0 */
        $h0 = (int) ($f0f0 + $f1f9_76 + $f2f8_38 + $f3f7_76 + $f4f6_38 + $f5f5_38) << 1;
        /** @var int $h1 */
        $h1 = (int) ($f0f1_2 + $f2f9_38 + $f3f8_38 + $f4f7_38 + $f5f6_38) << 1;
        /** @var int $h2 */
        $h2 = (int) ($f0f2_2 + $f1f1_2  + $f3f9_76 + $f4f8_38 + $f5f7_76 + $f6f6_19) << 1;
        /** @var int $h3 */
        $h3 = (int) ($f0f3_2 + $f1f2_2  + $f4f9_38 + $f5f8_38 + $f6f7_38) << 1;
        /** @var int $h4 */
        $h4 = (int) ($f0f4_2 + $f1f3_4  + $f2f2    + $f5f9_76 + $f6f8_38 + $f7f7_38) << 1;
        /** @var int $h5 */
        $h5 = (int) ($f0f5_2 + $f1f4_2  + $f2f3_2  + $f6f9_38 + $f7f8_38) << 1;
        /** @var int $h6 */
        $h6 = (int) ($f0f6_2 + $f1f5_4  + $f2f4_2  + $f3f3_2  + $f7f9_76 + $f8f8_19) << 1;
        /** @var int $h7 */
        $h7 = (int) ($f0f7_2 + $f1f6_2  + $f2f5_2  + $f3f4_2  + $f8f9_38) << 1;
        /** @var int $h8 */
        $h8 = (int) ($f0f8_2 + $f1f7_4  + $f2f6_2  + $f3f5_4  + $f4f4    + $f9f9_38) << 1;
        /** @var int $h9 */
        $h9 = (int) ($f0f9_2 + $f1f8_2  + $f2f7_2  + $f3f6_2  + $f4f5_2) << 1;

        /** @var int $carry0 */
        $carry0 = ($h0 + (1 << 25)) >> 26;
        $h1 += $carry0;
        $h0 -= $carry0 << 26;
        /** @var int $carry4 */
        $carry4 = ($h4 + (1 << 25)) >> 26;
        $h5 += $carry4;
        $h4 -= $carry4 << 26;

        /** @var int $carry1 */
        $carry1 = ($h1 + (1 << 24)) >> 25;
        $h2 += $carry1;
        $h1 -= $carry1 << 25;
        /** @var int $carry5 */
        $carry5 = ($h5 + (1 << 24)) >> 25;
        $h6 += $carry5;
        $h5 -= $carry5 << 25;

        /** @var int $carry2 */
        $carry2 = ($h2 + (1 << 25)) >> 26;
        $h3 += $carry2;
        $h2 -= $carry2 << 26;
        /** @var int $carry6 */
        $carry6 = ($h6 + (1 << 25)) >> 26;
        $h7 += $carry6;
        $h6 -= $carry6 << 26;

        /** @var int $carry3 */
        $carry3 = ($h3 + (1 << 24)) >> 25;
        $h4 += $carry3;
        $h3 -= $carry3 << 25;
        /** @var int $carry7 */
        $carry7 = ($h7 + (1 << 24)) >> 25;
        $h8 += $carry7;
        $h7 -= $carry7 << 25;

        /** @var int $carry4 */
        $carry4 = ($h4 + (1 << 25)) >> 26;
        $h5 += $carry4;
        $h4 -= $carry4 << 26;
        /** @var int $carry8 */
        $carry8 = ($h8 + (1 << 25)) >> 26;
        $h9 += $carry8;
        $h8 -= $carry8 << 26;

        /** @var int $carry9 */
        $carry9 = ($h9 + (1 << 24)) >> 25;
        $h0 += self::mul($carry9, 19, 5);
        $h9 -= $carry9 << 25;

        /** @var int $carry0 */
        $carry0 = ($h0 + (1 << 25)) >> 26;
        $h1 += $carry0;
        $h0 -= $carry0 << 26;

        return ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(
            array(
                (int) $h0,
                (int) $h1,
                (int) $h2,
                (int) $h3,
                (int) $h4,
                (int) $h5,
                (int) $h6,
                (int) $h7,
                (int) $h8,
                (int) $h9
            )
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $Z
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public static function fe_invert(ParagonIE_Sodium_Core_Curve25519_Fe $Z)
    {
        $z = clone $Z;
        $t0 = self::fe_sq($z);
        $t1 = self::fe_sq($t0);
        $t1 = self::fe_sq($t1);
        $t1 = self::fe_mul($z, $t1);
        $t0 = self::fe_mul($t0, $t1);
        $t2 = self::fe_sq($t0);
        $t1 = self::fe_mul($t1, $t2);
        $t2 = self::fe_sq($t1);
        for ($i = 1; $i < 5; ++$i) {
            $t2 = self::fe_sq($t2);
        }
        $t1 = self::fe_mul($t2, $t1);
        $t2 = self::fe_sq($t1);
        for ($i = 1; $i < 10; ++$i) {
            $t2 = self::fe_sq($t2);
        }
        $t2 = self::fe_mul($t2, $t1);
        $t3 = self::fe_sq($t2);
        for ($i = 1; $i < 20; ++$i) {
            $t3 = self::fe_sq($t3);
        }
        $t2 = self::fe_mul($t3, $t2);
        $t2 = self::fe_sq($t2);
        for ($i = 1; $i < 10; ++$i) {
            $t2 = self::fe_sq($t2);
        }
        $t1 = self::fe_mul($t2, $t1);
        $t2 = self::fe_sq($t1);
        for ($i = 1; $i < 50; ++$i) {
            $t2 = self::fe_sq($t2);
        }
        $t2 = self::fe_mul($t2, $t1);
        $t3 = self::fe_sq($t2);
        for ($i = 1; $i < 100; ++$i) {
            $t3 = self::fe_sq($t3);
        }
        $t2 = self::fe_mul($t3, $t2);
        $t2 = self::fe_sq($t2);
        for ($i = 1; $i < 50; ++$i) {
            $t2 = self::fe_sq($t2);
        }
        $t1 = self::fe_mul($t2, $t1);
        $t1 = self::fe_sq($t1);
        for ($i = 1; $i < 5; ++$i) {
            $t1 = self::fe_sq($t1);
        }
        return self::fe_mul($t1, $t0);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @ref https://github.com/jedisct1/libsodium/blob/68564326e1e9dc57ef03746f85734232d20ca6fb/src/libsodium/crypto_core/curve25519/ref10/curve25519_ref10.c#L1054-L1106
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $z
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public static function fe_pow22523(ParagonIE_Sodium_Core_Curve25519_Fe $z)
    {
        # fe_sq(t0, z);
        # fe_sq(t1, t0);
        # fe_sq(t1, t1);
        # fe_mul(t1, z, t1);
        # fe_mul(t0, t0, t1);
        # fe_sq(t0, t0);
        # fe_mul(t0, t1, t0);
        # fe_sq(t1, t0);
        $t0 = self::fe_sq($z);
        $t1 = self::fe_sq($t0);
        $t1 = self::fe_sq($t1);
        $t1 = self::fe_mul($z, $t1);
        $t0 = self::fe_mul($t0, $t1);
        $t0 = self::fe_sq($t0);
        $t0 = self::fe_mul($t1, $t0);
        $t1 = self::fe_sq($t0);

        # for (i = 1; i < 5; ++i) {
        #     fe_sq(t1, t1);
        # }
        for ($i = 1; $i < 5; ++$i) {
            $t1 = self::fe_sq($t1);
        }

        # fe_mul(t0, t1, t0);
        # fe_sq(t1, t0);
        $t0 = self::fe_mul($t1, $t0);
        $t1 = self::fe_sq($t0);

        # for (i = 1; i < 10; ++i) {
        #     fe_sq(t1, t1);
        # }
        for ($i = 1; $i < 10; ++$i) {
            $t1 = self::fe_sq($t1);
        }

        # fe_mul(t1, t1, t0);
        # fe_sq(t2, t1);
        $t1 = self::fe_mul($t1, $t0);
        $t2 = self::fe_sq($t1);

        # for (i = 1; i < 20; ++i) {
        #     fe_sq(t2, t2);
        # }
        for ($i = 1; $i < 20; ++$i) {
            $t2 = self::fe_sq($t2);
        }

        # fe_mul(t1, t2, t1);
        # fe_sq(t1, t1);
        $t1 = self::fe_mul($t2, $t1);
        $t1 = self::fe_sq($t1);

        # for (i = 1; i < 10; ++i) {
        #     fe_sq(t1, t1);
        # }
        for ($i = 1; $i < 10; ++$i) {
            $t1 = self::fe_sq($t1);
        }

        # fe_mul(t0, t1, t0);
        # fe_sq(t1, t0);
        $t0 = self::fe_mul($t1, $t0);
        $t1 = self::fe_sq($t0);

        # for (i = 1; i < 50; ++i) {
        #     fe_sq(t1, t1);
        # }
        for ($i = 1; $i < 50; ++$i) {
            $t1 = self::fe_sq($t1);
        }

        # fe_mul(t1, t1, t0);
        # fe_sq(t2, t1);
        $t1 = self::fe_mul($t1, $t0);
        $t2 = self::fe_sq($t1);

        # for (i = 1; i < 100; ++i) {
        #     fe_sq(t2, t2);
        # }
        for ($i = 1; $i < 100; ++$i) {
            $t2 = self::fe_sq($t2);
        }

        # fe_mul(t1, t2, t1);
        # fe_sq(t1, t1);
        $t1 = self::fe_mul($t2, $t1);
        $t1 = self::fe_sq($t1);

        # for (i = 1; i < 50; ++i) {
        #     fe_sq(t1, t1);
        # }
        for ($i = 1; $i < 50; ++$i) {
            $t1 = self::fe_sq($t1);
        }

        # fe_mul(t0, t1, t0);
        # fe_sq(t0, t0);
        # fe_sq(t0, t0);
        # fe_mul(out, t0, z);
        $t0 = self::fe_mul($t1, $t0);
        $t0 = self::fe_sq($t0);
        $t0 = self::fe_sq($t0);
        return self::fe_mul($t0, $z);
    }

    /**
     * Subtract two field elements.
     *
     * h = f - g
     *
     * Preconditions:
     * |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc.
     * |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc.
     *
     * Postconditions:
     * |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $g
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     * @psalm-suppress MixedOperand
     */
    public static function fe_sub(ParagonIE_Sodium_Core_Curve25519_Fe $f, ParagonIE_Sodium_Core_Curve25519_Fe $g)
    {
        return ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(
            array(
                (int) ($f[0] - $g[0]),
                (int) ($f[1] - $g[1]),
                (int) ($f[2] - $g[2]),
                (int) ($f[3] - $g[3]),
                (int) ($f[4] - $g[4]),
                (int) ($f[5] - $g[5]),
                (int) ($f[6] - $g[6]),
                (int) ($f[7] - $g[7]),
                (int) ($f[8] - $g[8]),
                (int) ($f[9] - $g[9])
            )
        );
    }

    /**
     * Add two group elements.
     *
     * r = p + q
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_Cached $q
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P1p1
     */
    public static function ge_add(
        ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p,
        ParagonIE_Sodium_Core_Curve25519_Ge_Cached $q
    ) {
        $r = new ParagonIE_Sodium_Core_Curve25519_Ge_P1p1();
        $r->X = self::fe_add($p->Y, $p->X);
        $r->Y = self::fe_sub($p->Y, $p->X);
        $r->Z = self::fe_mul($r->X, $q->YplusX);
        $r->Y = self::fe_mul($r->Y, $q->YminusX);
        $r->T = self::fe_mul($q->T2d, $p->T);
        $r->X = self::fe_mul($p->Z, $q->Z);
        $t0   = self::fe_add($r->X, $r->X);
        $r->X = self::fe_sub($r->Z, $r->Y);
        $r->Y = self::fe_add($r->Z, $r->Y);
        $r->Z = self::fe_add($t0, $r->T);
        $r->T = self::fe_sub($t0, $r->T);
        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @ref https://github.com/jedisct1/libsodium/blob/157c4a80c13b117608aeae12178b2d38825f9f8f/src/libsodium/crypto_core/curve25519/ref10/curve25519_ref10.c#L1185-L1215
     * @param string $a
     * @return array<int, mixed>
     * @throws SodiumException
     * @throws TypeError
     */
    public static function slide($a)
    {
        if (self::strlen($a) < 256) {
            if (self::strlen($a) < 16) {
                $a = str_pad($a, 256, '0', STR_PAD_RIGHT);
            }
        }
        /** @var array<int, int> $r */
        $r = array();

        /** @var int $i */
        for ($i = 0; $i < 256; ++$i) {
            $r[$i] = (int) (
                1 & (
                    self::chrToInt($a[(int) ($i >> 3)])
                        >>
                    ($i & 7)
                )
            );
        }

        for ($i = 0;$i < 256;++$i) {
            if ($r[$i]) {
                for ($b = 1;$b <= 6 && $i + $b < 256;++$b) {
                    if ($r[$i + $b]) {
                        if ($r[$i] + ($r[$i + $b] << $b) <= 15) {
                            $r[$i] += $r[$i + $b] << $b;
                            $r[$i + $b] = 0;
                        } elseif ($r[$i] - ($r[$i + $b] << $b) >= -15) {
                            $r[$i] -= $r[$i + $b] << $b;
                            for ($k = $i + $b; $k < 256; ++$k) {
                                if (!$r[$k]) {
                                    $r[$k] = 1;
                                    break;
                                }
                                $r[$k] = 0;
                            }
                        } else {
                            break;
                        }
                    }
                }
            }
        }
        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $s
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P3
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_frombytes_negate_vartime($s)
    {
        static $d = null;
        if (!$d) {
            $d = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$d);
        }

        # fe_frombytes(h->Y,s);
        # fe_1(h->Z);
        $h = new ParagonIE_Sodium_Core_Curve25519_Ge_P3(
            self::fe_0(),
            self::fe_frombytes($s),
            self::fe_1()
        );

        # fe_sq(u,h->Y);
        # fe_mul(v,u,d);
        # fe_sub(u,u,h->Z);       /* u = y^2-1 */
        # fe_add(v,v,h->Z);       /* v = dy^2+1 */
        $u = self::fe_sq($h->Y);
        /** @var ParagonIE_Sodium_Core_Curve25519_Fe $d */
        $v = self::fe_mul($u, $d);
        $u = self::fe_sub($u, $h->Z); /* u =  y^2 - 1 */
        $v = self::fe_add($v, $h->Z); /* v = dy^2 + 1 */

        # fe_sq(v3,v);
        # fe_mul(v3,v3,v);        /* v3 = v^3 */
        # fe_sq(h->X,v3);
        # fe_mul(h->X,h->X,v);
        # fe_mul(h->X,h->X,u);    /* x = uv^7 */
        $v3 = self::fe_sq($v);
        $v3 = self::fe_mul($v3, $v); /* v3 = v^3 */
        $h->X = self::fe_sq($v3);
        $h->X = self::fe_mul($h->X, $v);
        $h->X = self::fe_mul($h->X, $u); /* x = uv^7 */

        # fe_pow22523(h->X,h->X); /* x = (uv^7)^((q-5)/8) */
        # fe_mul(h->X,h->X,v3);
        # fe_mul(h->X,h->X,u);    /* x = uv^3(uv^7)^((q-5)/8) */
        $h->X = self::fe_pow22523($h->X); /* x = (uv^7)^((q-5)/8) */
        $h->X = self::fe_mul($h->X, $v3);
        $h->X = self::fe_mul($h->X, $u); /* x = uv^3(uv^7)^((q-5)/8) */

        # fe_sq(vxx,h->X);
        # fe_mul(vxx,vxx,v);
        # fe_sub(check,vxx,u);    /* vx^2-u */
        $vxx = self::fe_sq($h->X);
        $vxx = self::fe_mul($vxx, $v);
        $check = self::fe_sub($vxx, $u); /* vx^2 - u */

        # if (fe_isnonzero(check)) {
        #     fe_add(check,vxx,u);  /* vx^2+u */
        #     if (fe_isnonzero(check)) {
        #         return -1;
        #     }
        #     fe_mul(h->X,h->X,sqrtm1);
        # }
        if (self::fe_isnonzero($check)) {
            $check = self::fe_add($vxx, $u); /* vx^2 + u */
            if (self::fe_isnonzero($check)) {
                throw new RangeException('Internal check failed.');
            }
            $h->X = self::fe_mul(
                $h->X,
                ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$sqrtm1)
            );
        }

        # if (fe_isnegative(h->X) == (s[31] >> 7)) {
        #     fe_neg(h->X,h->X);
        # }
        $i = self::chrToInt($s[31]);
        if (self::fe_isnegative($h->X) === ($i >> 7)) {
            $h->X = self::fe_neg($h->X);
        }

        # fe_mul(h->T,h->X,h->Y);
        $h->T = self::fe_mul($h->X, $h->Y);
        return $h;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P1p1 $R
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_Precomp $q
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P1p1
     */
    public static function ge_madd(
        ParagonIE_Sodium_Core_Curve25519_Ge_P1p1 $R,
        ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p,
        ParagonIE_Sodium_Core_Curve25519_Ge_Precomp $q
    ) {
        $r = clone $R;
        $r->X = self::fe_add($p->Y, $p->X);
        $r->Y = self::fe_sub($p->Y, $p->X);
        $r->Z = self::fe_mul($r->X, $q->yplusx);
        $r->Y = self::fe_mul($r->Y, $q->yminusx);
        $r->T = self::fe_mul($q->xy2d, $p->T);
        $t0 = self::fe_add(clone $p->Z, clone $p->Z);
        $r->X = self::fe_sub($r->Z, $r->Y);
        $r->Y = self::fe_add($r->Z, $r->Y);
        $r->Z = self::fe_add($t0, $r->T);
        $r->T = self::fe_sub($t0, $r->T);

        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P1p1 $R
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_Precomp $q
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P1p1
     */
    public static function ge_msub(
        ParagonIE_Sodium_Core_Curve25519_Ge_P1p1 $R,
        ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p,
        ParagonIE_Sodium_Core_Curve25519_Ge_Precomp $q
    ) {
        $r = clone $R;

        $r->X = self::fe_add($p->Y, $p->X);
        $r->Y = self::fe_sub($p->Y, $p->X);
        $r->Z = self::fe_mul($r->X, $q->yminusx);
        $r->Y = self::fe_mul($r->Y, $q->yplusx);
        $r->T = self::fe_mul($q->xy2d, $p->T);
        $t0 = self::fe_add($p->Z, $p->Z);
        $r->X = self::fe_sub($r->Z, $r->Y);
        $r->Y = self::fe_add($r->Z, $r->Y);
        $r->Z = self::fe_sub($t0, $r->T);
        $r->T = self::fe_add($t0, $r->T);

        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P1p1 $p
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P2
     */
    public static function ge_p1p1_to_p2(ParagonIE_Sodium_Core_Curve25519_Ge_P1p1 $p)
    {
        $r = new ParagonIE_Sodium_Core_Curve25519_Ge_P2();
        $r->X = self::fe_mul($p->X, $p->T);
        $r->Y = self::fe_mul($p->Y, $p->Z);
        $r->Z = self::fe_mul($p->Z, $p->T);
        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P1p1 $p
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P3
     */
    public static function ge_p1p1_to_p3(ParagonIE_Sodium_Core_Curve25519_Ge_P1p1 $p)
    {
        $r = new ParagonIE_Sodium_Core_Curve25519_Ge_P3();
        $r->X = self::fe_mul($p->X, $p->T);
        $r->Y = self::fe_mul($p->Y, $p->Z);
        $r->Z = self::fe_mul($p->Z, $p->T);
        $r->T = self::fe_mul($p->X, $p->Y);
        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P2
     */
    public static function ge_p2_0()
    {
        return new ParagonIE_Sodium_Core_Curve25519_Ge_P2(
            self::fe_0(),
            self::fe_1(),
            self::fe_1()
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P2 $p
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P1p1
     */
    public static function ge_p2_dbl(ParagonIE_Sodium_Core_Curve25519_Ge_P2 $p)
    {
        $r = new ParagonIE_Sodium_Core_Curve25519_Ge_P1p1();

        $r->X = self::fe_sq($p->X);
        $r->Z = self::fe_sq($p->Y);
        $r->T = self::fe_sq2($p->Z);
        $r->Y = self::fe_add($p->X, $p->Y);
        $t0   = self::fe_sq($r->Y);
        $r->Y = self::fe_add($r->Z, $r->X);
        $r->Z = self::fe_sub($r->Z, $r->X);
        $r->X = self::fe_sub($t0, $r->Y);
        $r->T = self::fe_sub($r->T, $r->Z);

        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P3
     */
    public static function ge_p3_0()
    {
        return new ParagonIE_Sodium_Core_Curve25519_Ge_P3(
            self::fe_0(),
            self::fe_1(),
            self::fe_1(),
            self::fe_0()
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_Cached
     */
    public static function ge_p3_to_cached(ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p)
    {
        static $d2 = null;
        if ($d2 === null) {
            $d2 = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$d2);
        }
        /** @var ParagonIE_Sodium_Core_Curve25519_Fe $d2 */
        $r = new ParagonIE_Sodium_Core_Curve25519_Ge_Cached();
        $r->YplusX = self::fe_add($p->Y, $p->X);
        $r->YminusX = self::fe_sub($p->Y, $p->X);
        $r->Z = self::fe_copy($p->Z);
        $r->T2d = self::fe_mul($p->T, $d2);
        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P2
     */
    public static function ge_p3_to_p2(ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p)
    {
        return new ParagonIE_Sodium_Core_Curve25519_Ge_P2(
            self::fe_copy($p->X),
            self::fe_copy($p->Y),
            self::fe_copy($p->Z)
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $h
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_p3_tobytes(ParagonIE_Sodium_Core_Curve25519_Ge_P3 $h)
    {
        $recip = self::fe_invert($h->Z);
        $x = self::fe_mul($h->X, $recip);
        $y = self::fe_mul($h->Y, $recip);
        $s = self::fe_tobytes($y);
        $s[31] = self::intToChr(
            self::chrToInt($s[31]) ^ (self::fe_isnegative($x) << 7)
        );
        return $s;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P1p1
     */
    public static function ge_p3_dbl(ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p)
    {
        $q = self::ge_p3_to_p2($p);
        return self::ge_p2_dbl($q);
    }

    /**
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_Precomp
     */
    public static function ge_precomp_0()
    {
        return new ParagonIE_Sodium_Core_Curve25519_Ge_Precomp(
            self::fe_1(),
            self::fe_1(),
            self::fe_0()
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $b
     * @param int $c
     * @return int
     */
    public static function equal($b, $c)
    {
        return (int) ((($b ^ $c) - 1) >> 31) & 1;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int|string $char
     * @return int (1 = yes, 0 = no)
     * @throws SodiumException
     * @throws TypeError
     */
    public static function negative($char)
    {
        if (is_int($char)) {
            return ($char >> 63) & 1;
        }
        $x = self::chrToInt(self::substr($char, 0, 1));
        return (int) ($x >> 63);
    }

    /**
     * Conditional move
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_Precomp $t
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_Precomp $u
     * @param int $b
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_Precomp
     */
    public static function cmov(
        ParagonIE_Sodium_Core_Curve25519_Ge_Precomp $t,
        ParagonIE_Sodium_Core_Curve25519_Ge_Precomp $u,
        $b
    ) {
        if (!is_int($b)) {
            throw new InvalidArgumentException('Expected an integer.');
        }
        return new ParagonIE_Sodium_Core_Curve25519_Ge_Precomp(
            self::fe_cmov($t->yplusx,  $u->yplusx,  $b),
            self::fe_cmov($t->yminusx, $u->yminusx, $b),
            self::fe_cmov($t->xy2d,    $u->xy2d,    $b)
        );
    }

    /**
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_Cached $t
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_Cached $u
     * @param int $b
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_Cached
     */
    public static function ge_cmov_cached(
        ParagonIE_Sodium_Core_Curve25519_Ge_Cached $t,
        ParagonIE_Sodium_Core_Curve25519_Ge_Cached $u,
        $b
    ) {
        $b &= 1;
        $ret = new ParagonIE_Sodium_Core_Curve25519_Ge_Cached();
        $ret->YplusX  = self::fe_cmov($t->YplusX,  $u->YplusX,  $b);
        $ret->YminusX = self::fe_cmov($t->YminusX, $u->YminusX, $b);
        $ret->Z       = self::fe_cmov($t->Z,       $u->Z,       $b);
        $ret->T2d     = self::fe_cmov($t->T2d,     $u->T2d,     $b);
        return $ret;
    }

    /**
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_Cached[] $cached
     * @param int $b
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_Cached
     * @throws SodiumException
     */
    public static function ge_cmov8_cached(array $cached, $b)
    {
        // const unsigned char bnegative = negative(b);
        // const unsigned char babs      = b - (((-bnegative) & b) * ((signed char) 1 << 1));
        $bnegative = self::negative($b);
        $babs = $b - (((-$bnegative) & $b) << 1);

        // ge25519_cached_0(t);
        $t = new ParagonIE_Sodium_Core_Curve25519_Ge_Cached(
            self::fe_1(),
            self::fe_1(),
            self::fe_1(),
            self::fe_0()
        );

        // ge25519_cmov_cached(t, &cached[0], equal(babs, 1));
        // ge25519_cmov_cached(t, &cached[1], equal(babs, 2));
        // ge25519_cmov_cached(t, &cached[2], equal(babs, 3));
        // ge25519_cmov_cached(t, &cached[3], equal(babs, 4));
        // ge25519_cmov_cached(t, &cached[4], equal(babs, 5));
        // ge25519_cmov_cached(t, &cached[5], equal(babs, 6));
        // ge25519_cmov_cached(t, &cached[6], equal(babs, 7));
        // ge25519_cmov_cached(t, &cached[7], equal(babs, 8));
        for ($x = 0; $x < 8; ++$x) {
            $t = self::ge_cmov_cached($t, $cached[$x], self::equal($babs, $x + 1));
        }

        // fe25519_copy(minust.YplusX, t->YminusX);
        // fe25519_copy(minust.YminusX, t->YplusX);
        // fe25519_copy(minust.Z, t->Z);
        // fe25519_neg(minust.T2d, t->T2d);
        $minust = new ParagonIE_Sodium_Core_Curve25519_Ge_Cached(
            self::fe_copy($t->YminusX),
            self::fe_copy($t->YplusX),
            self::fe_copy($t->Z),
            self::fe_neg($t->T2d)
        );
        return self::ge_cmov_cached($t, $minust, $bnegative);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $pos
     * @param int $b
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_Precomp
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayOffset
     */
    public static function ge_select($pos = 0, $b = 0)
    {
        static $base = null;
        if ($base === null) {
            $base = array();
            /** @var int $i */
            foreach (self::$base as $i => $bas) {
                for ($j = 0; $j < 8; ++$j) {
                    $base[$i][$j] = new ParagonIE_Sodium_Core_Curve25519_Ge_Precomp(
                        ParagonIE_Sodium_Core_Curve25519_Fe::fromArray($bas[$j][0]),
                        ParagonIE_Sodium_Core_Curve25519_Fe::fromArray($bas[$j][1]),
                        ParagonIE_Sodium_Core_Curve25519_Fe::fromArray($bas[$j][2])
                    );
                }
            }
        }
        /** @var array<int, array<int, ParagonIE_Sodium_Core_Curve25519_Ge_Precomp>> $base */
        if (!is_int($pos)) {
            throw new InvalidArgumentException('Position must be an integer');
        }
        if ($pos < 0 || $pos > 31) {
            throw new RangeException('Position is out of range [0, 31]');
        }

        /** @var int $bnegative */
        $bnegative = self::negative($b);
        /** @var int $babs */
        $babs = $b - (((-$bnegative) & $b) << 1);

        $t = self::ge_precomp_0();
        for ($i = 0; $i < 8; ++$i) {
            $t = self::cmov(
                $t,
                $base[$pos][$i],
                self::equal($babs, $i + 1)
            );
        }
        $minusT = new ParagonIE_Sodium_Core_Curve25519_Ge_Precomp(
            self::fe_copy($t->yminusx),
            self::fe_copy($t->yplusx),
            self::fe_neg($t->xy2d)
        );
        return self::cmov($t, $minusT, $bnegative);
    }

    /**
     * Subtract two group elements.
     *
     * r = p - q
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_Cached $q
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P1p1
     */
    public static function ge_sub(
        ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p,
        ParagonIE_Sodium_Core_Curve25519_Ge_Cached $q
    ) {
        $r = new ParagonIE_Sodium_Core_Curve25519_Ge_P1p1();

        $r->X = self::fe_add($p->Y, $p->X);
        $r->Y = self::fe_sub($p->Y, $p->X);
        $r->Z = self::fe_mul($r->X, $q->YminusX);
        $r->Y = self::fe_mul($r->Y, $q->YplusX);
        $r->T = self::fe_mul($q->T2d, $p->T);
        $r->X = self::fe_mul($p->Z, $q->Z);
        $t0 = self::fe_add($r->X, $r->X);
        $r->X = self::fe_sub($r->Z, $r->Y);
        $r->Y = self::fe_add($r->Z, $r->Y);
        $r->Z = self::fe_sub($t0, $r->T);
        $r->T = self::fe_add($t0, $r->T);

        return $r;
    }

    /**
     * Convert a group element to a byte string.
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P2 $h
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_tobytes(ParagonIE_Sodium_Core_Curve25519_Ge_P2 $h)
    {
        $recip = self::fe_invert($h->Z);
        $x = self::fe_mul($h->X, $recip);
        $y = self::fe_mul($h->Y, $recip);
        $s = self::fe_tobytes($y);
        $s[31] = self::intToChr(
            self::chrToInt($s[31]) ^ (self::fe_isnegative($x) << 7)
        );
        return $s;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $a
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A
     * @param string $b
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P2
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedArrayAccess
     */
    public static function ge_double_scalarmult_vartime(
        $a,
        ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A,
        $b
    ) {
        /** @var array<int, ParagonIE_Sodium_Core_Curve25519_Ge_Cached> $Ai */
        $Ai = array();

        /** @var array<int, ParagonIE_Sodium_Core_Curve25519_Ge_Precomp> $Bi */
        static $Bi = array();
        if (!$Bi) {
            for ($i = 0; $i < 8; ++$i) {
                $Bi[$i] = new ParagonIE_Sodium_Core_Curve25519_Ge_Precomp(
                    ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$base2[$i][0]),
                    ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$base2[$i][1]),
                    ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$base2[$i][2])
                );
            }
        }
        for ($i = 0; $i < 8; ++$i) {
            $Ai[$i] = new ParagonIE_Sodium_Core_Curve25519_Ge_Cached(
                self::fe_0(),
                self::fe_0(),
                self::fe_0(),
                self::fe_0()
            );
        }

        # slide(aslide,a);
        # slide(bslide,b);
        /** @var array<int, int> $aslide */
        $aslide = self::slide($a);
        /** @var array<int, int> $bslide */
        $bslide = self::slide($b);

        # ge_p3_to_cached(&Ai[0],A);
        # ge_p3_dbl(&t,A); ge_p1p1_to_p3(&A2,&t);
        $Ai[0] = self::ge_p3_to_cached($A);
        $t = self::ge_p3_dbl($A);
        $A2 = self::ge_p1p1_to_p3($t);

        # ge_add(&t,&A2,&Ai[0]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[1],&u);
        # ge_add(&t,&A2,&Ai[1]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[2],&u);
        # ge_add(&t,&A2,&Ai[2]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[3],&u);
        # ge_add(&t,&A2,&Ai[3]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[4],&u);
        # ge_add(&t,&A2,&Ai[4]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[5],&u);
        # ge_add(&t,&A2,&Ai[5]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[6],&u);
        # ge_add(&t,&A2,&Ai[6]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[7],&u);
        for ($i = 0; $i < 7; ++$i) {
            $t = self::ge_add($A2, $Ai[$i]);
            $u = self::ge_p1p1_to_p3($t);
            $Ai[$i + 1] = self::ge_p3_to_cached($u);
        }

        # ge_p2_0(r);
        $r = self::ge_p2_0();

        # for (i = 255;i >= 0;--i) {
        #     if (aslide[i] || bslide[i]) break;
        # }
        $i = 255;
        for (; $i >= 0; --$i) {
            if ($aslide[$i] || $bslide[$i]) {
                break;
            }
        }

        # for (;i >= 0;--i) {
        for (; $i >= 0; --$i) {
            # ge_p2_dbl(&t,r);
            $t = self::ge_p2_dbl($r);

            # if (aslide[i] > 0) {
            if ($aslide[$i] > 0) {
                # ge_p1p1_to_p3(&u,&t);
                # ge_add(&t,&u,&Ai[aslide[i]/2]);
                $u = self::ge_p1p1_to_p3($t);
                $t = self::ge_add(
                    $u,
                    $Ai[(int) floor($aslide[$i] / 2)]
                );
            # } else if (aslide[i] < 0) {
            } elseif ($aslide[$i] < 0) {
                # ge_p1p1_to_p3(&u,&t);
                # ge_sub(&t,&u,&Ai[(-aslide[i])/2]);
                $u = self::ge_p1p1_to_p3($t);
                $t = self::ge_sub(
                    $u,
                    $Ai[(int) floor(-$aslide[$i] / 2)]
                );
            }

            # if (bslide[i] > 0) {
            if ($bslide[$i] > 0) {
                /** @var int $index */
                $index = (int) floor($bslide[$i] / 2);
                # ge_p1p1_to_p3(&u,&t);
                # ge_madd(&t,&u,&Bi[bslide[i]/2]);
                $u = self::ge_p1p1_to_p3($t);
                $t = self::ge_madd($t, $u, $Bi[$index]);
            # } else if (bslide[i] < 0) {
            } elseif ($bslide[$i] < 0) {
                /** @var int $index */
                $index = (int) floor(-$bslide[$i] / 2);
                # ge_p1p1_to_p3(&u,&t);
                # ge_msub(&t,&u,&Bi[(-bslide[i])/2]);
                $u = self::ge_p1p1_to_p3($t);
                $t = self::ge_msub($t, $u, $Bi[$index]);
            }
            # ge_p1p1_to_p2(r,&t);
            $r = self::ge_p1p1_to_p2($t);
        }
        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $a
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $p
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P3
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedOperand
     */
    public static function ge_scalarmult($a, $p)
    {
        $e = array_fill(0, 64, 0);

        /** @var ParagonIE_Sodium_Core_Curve25519_Ge_Cached[] $pi */
        $pi = array();

        //        ge25519_p3_to_cached(&pi[1 - 1], p);   /* p */
        $pi[0] = self::ge_p3_to_cached($p);

        //        ge25519_p3_dbl(&t2, p);
        //        ge25519_p1p1_to_p3(&p2, &t2);
        //        ge25519_p3_to_cached(&pi[2 - 1], &p2); /* 2p = 2*p */
        $t2 = self::ge_p3_dbl($p);
        $p2 = self::ge_p1p1_to_p3($t2);
        $pi[1] = self::ge_p3_to_cached($p2);

        //        ge25519_add_cached(&t3, p, &pi[2 - 1]);
        //        ge25519_p1p1_to_p3(&p3, &t3);
        //        ge25519_p3_to_cached(&pi[3 - 1], &p3); /* 3p = 2p+p */
        $t3 = self::ge_add($p, $pi[1]);
        $p3 = self::ge_p1p1_to_p3($t3);
        $pi[2] = self::ge_p3_to_cached($p3);

        //        ge25519_p3_dbl(&t4, &p2);
        //        ge25519_p1p1_to_p3(&p4, &t4);
        //        ge25519_p3_to_cached(&pi[4 - 1], &p4); /* 4p = 2*2p */
        $t4 = self::ge_p3_dbl($p2);
        $p4 = self::ge_p1p1_to_p3($t4);
        $pi[3] = self::ge_p3_to_cached($p4);

        //        ge25519_add_cached(&t5, p, &pi[4 - 1]);
        //        ge25519_p1p1_to_p3(&p5, &t5);
        //        ge25519_p3_to_cached(&pi[5 - 1], &p5); /* 5p = 4p+p */
        $t5 = self::ge_add($p, $pi[3]);
        $p5 = self::ge_p1p1_to_p3($t5);
        $pi[4] = self::ge_p3_to_cached($p5);

        //        ge25519_p3_dbl(&t6, &p3);
        //        ge25519_p1p1_to_p3(&p6, &t6);
        //        ge25519_p3_to_cached(&pi[6 - 1], &p6); /* 6p = 2*3p */
        $t6 = self::ge_p3_dbl($p3);
        $p6 = self::ge_p1p1_to_p3($t6);
        $pi[5] = self::ge_p3_to_cached($p6);

        //        ge25519_add_cached(&t7, p, &pi[6 - 1]);
        //        ge25519_p1p1_to_p3(&p7, &t7);
        //        ge25519_p3_to_cached(&pi[7 - 1], &p7); /* 7p = 6p+p */
        $t7 = self::ge_add($p, $pi[5]);
        $p7 = self::ge_p1p1_to_p3($t7);
        $pi[6] = self::ge_p3_to_cached($p7);

        //        ge25519_p3_dbl(&t8, &p4);
        //        ge25519_p1p1_to_p3(&p8, &t8);
        //        ge25519_p3_to_cached(&pi[8 - 1], &p8); /* 8p = 2*4p */
        $t8 = self::ge_p3_dbl($p4);
        $p8 = self::ge_p1p1_to_p3($t8);
        $pi[7] = self::ge_p3_to_cached($p8);


        //        for (i = 0; i < 32; ++i) {
        //            e[2 * i + 0] = (a[i] >> 0) & 15;
        //            e[2 * i + 1] = (a[i] >> 4) & 15;
        //        }
        for ($i = 0; $i < 32; ++$i) {
            $e[($i << 1)    ] =  self::chrToInt($a[$i]) & 15;
            $e[($i << 1) + 1] = (self::chrToInt($a[$i]) >> 4) & 15;
        }
        //        /* each e[i] is between 0 and 15 */
        //        /* e[63] is between 0 and 7 */

        //        carry = 0;
        //        for (i = 0; i < 63; ++i) {
        //            e[i] += carry;
        //            carry = e[i] + 8;
        //            carry >>= 4;
        //            e[i] -= carry * ((signed char) 1 << 4);
        //        }
        $carry = 0;
        for ($i = 0; $i < 64; ++$i) {
            $e[$i] += $carry;
            $carry = $e[$i] + 8;
            $carry >>= 4;
            $e[$i] -= $carry << 4;
        }
        //        e[63] += carry;
        //        /* each e[i] is between -8 and 8 */
        $e[63] += $carry;

        //        ge25519_p3_0(h);
        $h = self::ge_p3_0();

        //        for (i = 63; i != 0; i--) {
        for ($i = 63; $i != 0; --$i) {
            // ge25519_cmov8_cached(&t, pi, e[i]);
            $t = self::ge_cmov8_cached($pi, $e[$i]);
            // ge25519_add_cached(&r, h, &t);
            $r = self::ge_add($h, $t);

            // ge25519_p1p1_to_p2(&s, &r);
            // ge25519_p2_dbl(&r, &s);
            // ge25519_p1p1_to_p2(&s, &r);
            // ge25519_p2_dbl(&r, &s);
            // ge25519_p1p1_to_p2(&s, &r);
            // ge25519_p2_dbl(&r, &s);
            // ge25519_p1p1_to_p2(&s, &r);
            // ge25519_p2_dbl(&r, &s);
            $s = self::ge_p1p1_to_p2($r);
            $r = self::ge_p2_dbl($s);
            $s = self::ge_p1p1_to_p2($r);
            $r = self::ge_p2_dbl($s);
            $s = self::ge_p1p1_to_p2($r);
            $r = self::ge_p2_dbl($s);
            $s = self::ge_p1p1_to_p2($r);
            $r = self::ge_p2_dbl($s);

            // ge25519_p1p1_to_p3(h, &r);  /* *16 */
            $h = self::ge_p1p1_to_p3($r); /* *16 */
        }

        //        ge25519_cmov8_cached(&t, pi, e[i]);
        //        ge25519_add_cached(&r, h, &t);
        //        ge25519_p1p1_to_p3(h, &r);
        $t = self::ge_cmov8_cached($pi, $e[0]);
        $r = self::ge_add($h, $t);
        return self::ge_p1p1_to_p3($r);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $a
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P3
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedOperand
     */
    public static function ge_scalarmult_base($a)
    {
        /** @var array<int, int> $e */
        $e = array();
        $r = new ParagonIE_Sodium_Core_Curve25519_Ge_P1p1();

        for ($i = 0; $i < 32; ++$i) {
            /** @var int $dbl */
            $dbl = (int) $i << 1;
            $e[$dbl] = (int) self::chrToInt($a[$i]) & 15;
            $e[$dbl + 1] = (int) (self::chrToInt($a[$i]) >> 4) & 15;
        }

        /** @var int $carry */
        $carry = 0;
        for ($i = 0; $i < 63; ++$i) {
            $e[$i] += $carry;
            /** @var int $carry */
            $carry = $e[$i] + 8;
            /** @var int $carry */
            $carry >>= 4;
            $e[$i] -= $carry << 4;
        }
        /** @var array<int, int> $e */
        $e[63] += (int) $carry;

        $h = self::ge_p3_0();

        for ($i = 1; $i < 64; $i += 2) {
            $t = self::ge_select((int) floor($i / 2), (int) $e[$i]);
            $r = self::ge_madd($r, $h, $t);
            $h = self::ge_p1p1_to_p3($r);
        }

        $r = self::ge_p3_dbl($h);

        $s = self::ge_p1p1_to_p2($r);
        $r = self::ge_p2_dbl($s);
        $s = self::ge_p1p1_to_p2($r);
        $r = self::ge_p2_dbl($s);
        $s = self::ge_p1p1_to_p2($r);
        $r = self::ge_p2_dbl($s);

        $h = self::ge_p1p1_to_p3($r);

        for ($i = 0; $i < 64; $i += 2) {
            $t = self::ge_select($i >> 1, (int) $e[$i]);
            $r = self::ge_madd($r, $h, $t);
            $h = self::ge_p1p1_to_p3($r);
        }
        return $h;
    }

    /**
     * Calculates (ab + c) mod l
     * where l = 2^252 + 27742317777372353535851937790883648493
     *
     * @internal You should not use this directly from another application
     *
     * @param string $a
     * @param string $b
     * @param string $c
     * @return string
     * @throws TypeError
     */
    public static function sc_muladd($a, $b, $c)
    {
        /** @var int $a0 */
        $a0 = 2097151 & self::load_3(self::substr($a, 0, 3));
        /** @var int $a1 */
        $a1 = 2097151 & (self::load_4(self::substr($a, 2, 4)) >> 5);
        /** @var int $a2 */
        $a2 = 2097151 & (self::load_3(self::substr($a, 5, 3)) >> 2);
        /** @var int $a3 */
        $a3 = 2097151 & (self::load_4(self::substr($a, 7, 4)) >> 7);
        /** @var int $a4 */
        $a4 = 2097151 & (self::load_4(self::substr($a, 10, 4)) >> 4);
        /** @var int $a5 */
        $a5 = 2097151 & (self::load_3(self::substr($a, 13, 3)) >> 1);
        /** @var int $a6 */
        $a6 = 2097151 & (self::load_4(self::substr($a, 15, 4)) >> 6);
        /** @var int $a7 */
        $a7 = 2097151 & (self::load_3(self::substr($a, 18, 3)) >> 3);
        /** @var int $a8 */
        $a8 = 2097151 & self::load_3(self::substr($a, 21, 3));
        /** @var int $a9 */
        $a9 = 2097151 & (self::load_4(self::substr($a, 23, 4)) >> 5);
        /** @var int $a10 */
        $a10 = 2097151 & (self::load_3(self::substr($a, 26, 3)) >> 2);
        /** @var int $a11 */
        $a11 = (self::load_4(self::substr($a, 28, 4)) >> 7);

        /** @var int $b0 */
        $b0 = 2097151 & self::load_3(self::substr($b, 0, 3));
        /** @var int $b1 */
        $b1 = 2097151 & (self::load_4(self::substr($b, 2, 4)) >> 5);
        /** @var int $b2 */
        $b2 = 2097151 & (self::load_3(self::substr($b, 5, 3)) >> 2);
        /** @var int $b3 */
        $b3 = 2097151 & (self::load_4(self::substr($b, 7, 4)) >> 7);
        /** @var int $b4 */
        $b4 = 2097151 & (self::load_4(self::substr($b, 10, 4)) >> 4);
        /** @var int $b5 */
        $b5 = 2097151 & (self::load_3(self::substr($b, 13, 3)) >> 1);
        /** @var int $b6 */
        $b6 = 2097151 & (self::load_4(self::substr($b, 15, 4)) >> 6);
        /** @var int $b7 */
        $b7 = 2097151 & (self::load_3(self::substr($b, 18, 3)) >> 3);
        /** @var int $b8 */
        $b8 = 2097151 & self::load_3(self::substr($b, 21, 3));
        /** @var int $b9 */
        $b9 = 2097151 & (self::load_4(self::substr($b, 23, 4)) >> 5);
        /** @var int $b10 */
        $b10 = 2097151 & (self::load_3(self::substr($b, 26, 3)) >> 2);
        /** @var int $b11 */
        $b11 = (self::load_4(self::substr($b, 28, 4)) >> 7);

        /** @var int $c0 */
        $c0 = 2097151 & self::load_3(self::substr($c, 0, 3));
        /** @var int $c1 */
        $c1 = 2097151 & (self::load_4(self::substr($c, 2, 4)) >> 5);
        /** @var int $c2 */
        $c2 = 2097151 & (self::load_3(self::substr($c, 5, 3)) >> 2);
        /** @var int $c3 */
        $c3 = 2097151 & (self::load_4(self::substr($c, 7, 4)) >> 7);
        /** @var int $c4 */
        $c4 = 2097151 & (self::load_4(self::substr($c, 10, 4)) >> 4);
        /** @var int $c5 */
        $c5 = 2097151 & (self::load_3(self::substr($c, 13, 3)) >> 1);
        /** @var int $c6 */
        $c6 = 2097151 & (self::load_4(self::substr($c, 15, 4)) >> 6);
        /** @var int $c7 */
        $c7 = 2097151 & (self::load_3(self::substr($c, 18, 3)) >> 3);
        /** @var int $c8 */
        $c8 = 2097151 & self::load_3(self::substr($c, 21, 3));
        /** @var int $c9 */
        $c9 = 2097151 & (self::load_4(self::substr($c, 23, 4)) >> 5);
        /** @var int $c10 */
        $c10 = 2097151 & (self::load_3(self::substr($c, 26, 3)) >> 2);
        /** @var int $c11 */
        $c11 = (self::load_4(self::substr($c, 28, 4)) >> 7);

        /* Can't really avoid the pyramid here: */
        $s0 = $c0 + self::mul($a0, $b0, 24);
        $s1 = $c1 + self::mul($a0, $b1, 24) + self::mul($a1, $b0, 24);
        $s2 = $c2 + self::mul($a0, $b2, 24) + self::mul($a1, $b1, 24) + self::mul($a2, $b0, 24);
        $s3 = $c3 + self::mul($a0, $b3, 24) + self::mul($a1, $b2, 24) + self::mul($a2, $b1, 24) + self::mul($a3, $b0, 24);
        $s4 = $c4 + self::mul($a0, $b4, 24) + self::mul($a1, $b3, 24) + self::mul($a2, $b2, 24) + self::mul($a3, $b1, 24) +
               self::mul($a4, $b0, 24);
        $s5 = $c5 + self::mul($a0, $b5, 24) + self::mul($a1, $b4, 24) + self::mul($a2, $b3, 24) + self::mul($a3, $b2, 24) +
               self::mul($a4, $b1, 24) + self::mul($a5, $b0, 24);
        $s6 = $c6 + self::mul($a0, $b6, 24) + self::mul($a1, $b5, 24) + self::mul($a2, $b4, 24) + self::mul($a3, $b3, 24) +
               self::mul($a4, $b2, 24) + self::mul($a5, $b1, 24) + self::mul($a6, $b0, 24);
        $s7 = $c7 + self::mul($a0, $b7, 24) + self::mul($a1, $b6, 24) + self::mul($a2, $b5, 24) + self::mul($a3, $b4, 24) +
               self::mul($a4, $b3, 24) + self::mul($a5, $b2, 24) + self::mul($a6, $b1, 24) + self::mul($a7, $b0, 24);
        $s8 = $c8 + self::mul($a0, $b8, 24) + self::mul($a1, $b7, 24) + self::mul($a2, $b6, 24) + self::mul($a3, $b5, 24) +
               self::mul($a4, $b4, 24) + self::mul($a5, $b3, 24) + self::mul($a6, $b2, 24) + self::mul($a7, $b1, 24) +
               self::mul($a8, $b0, 24);
        $s9 = $c9 + self::mul($a0, $b9, 24) + self::mul($a1, $b8, 24) + self::mul($a2, $b7, 24) + self::mul($a3, $b6, 24) +
               self::mul($a4, $b5, 24) + self::mul($a5, $b4, 24) + self::mul($a6, $b3, 24) + self::mul($a7, $b2, 24) +
               self::mul($a8, $b1, 24) + self::mul($a9, $b0, 24);
        $s10 = $c10 + self::mul($a0, $b10, 24) + self::mul($a1, $b9, 24) + self::mul($a2, $b8, 24) + self::mul($a3, $b7, 24) +
               self::mul($a4, $b6, 24) + self::mul($a5, $b5, 24) + self::mul($a6, $b4, 24) + self::mul($a7, $b3, 24) +
               self::mul($a8, $b2, 24) + self::mul($a9, $b1, 24) + self::mul($a10, $b0, 24);
        $s11 = $c11 + self::mul($a0, $b11, 24) + self::mul($a1, $b10, 24) + self::mul($a2, $b9, 24) + self::mul($a3, $b8, 24) +
               self::mul($a4, $b7, 24) + self::mul($a5, $b6, 24) + self::mul($a6, $b5, 24) + self::mul($a7, $b4, 24) +
               self::mul($a8, $b3, 24) + self::mul($a9, $b2, 24) + self::mul($a10, $b1, 24) + self::mul($a11, $b0, 24);
        $s12 = self::mul($a1, $b11, 24) + self::mul($a2, $b10, 24) + self::mul($a3, $b9, 24) + self::mul($a4, $b8, 24) +
               self::mul($a5, $b7, 24) + self::mul($a6, $b6, 24) + self::mul($a7, $b5, 24) + self::mul($a8, $b4, 24) +
               self::mul($a9, $b3, 24) + self::mul($a10, $b2, 24) + self::mul($a11, $b1, 24);
        $s13 = self::mul($a2, $b11, 24) + self::mul($a3, $b10, 24) + self::mul($a4, $b9, 24) + self::mul($a5, $b8, 24) +
               self::mul($a6, $b7, 24) + self::mul($a7, $b6, 24) + self::mul($a8, $b5, 24) + self::mul($a9, $b4, 24) +
               self::mul($a10, $b3, 24) + self::mul($a11, $b2, 24);
        $s14 = self::mul($a3, $b11, 24) + self::mul($a4, $b10, 24) + self::mul($a5, $b9, 24) + self::mul($a6, $b8, 24) +
               self::mul($a7, $b7, 24) + self::mul($a8, $b6, 24) + self::mul($a9, $b5, 24) + self::mul($a10, $b4, 24) +
               self::mul($a11, $b3, 24);
        $s15 = self::mul($a4, $b11, 24) + self::mul($a5, $b10, 24) + self::mul($a6, $b9, 24) + self::mul($a7, $b8, 24) +
               self::mul($a8, $b7, 24) + self::mul($a9, $b6, 24) + self::mul($a10, $b5, 24) + self::mul($a11, $b4, 24);
        $s16 = self::mul($a5, $b11, 24) + self::mul($a6, $b10, 24) + self::mul($a7, $b9, 24) + self::mul($a8, $b8, 24) +
               self::mul($a9, $b7, 24) + self::mul($a10, $b6, 24) + self::mul($a11, $b5, 24);
        $s17 = self::mul($a6, $b11, 24) + self::mul($a7, $b10, 24) + self::mul($a8, $b9, 24) + self::mul($a9, $b8, 24) +
               self::mul($a10, $b7, 24) + self::mul($a11, $b6, 24);
        $s18 = self::mul($a7, $b11, 24) + self::mul($a8, $b10, 24) + self::mul($a9, $b9, 24) + self::mul($a10, $b8, 24) +
               self::mul($a11, $b7, 24);
        $s19 = self::mul($a8, $b11, 24) + self::mul($a9, $b10, 24) + self::mul($a10, $b9, 24) + self::mul($a11, $b8, 24);
        $s20 = self::mul($a9, $b11, 24) + self::mul($a10, $b10, 24) + self::mul($a11, $b9, 24);
        $s21 = self::mul($a10, $b11, 24) + self::mul($a11, $b10, 24);
        $s22 = self::mul($a11, $b11, 24);
        $s23 = 0;

        /** @var int $carry0 */
        $carry0 = ($s0 + (1 << 20)) >> 21;
        $s1 += $carry0;
        $s0 -= $carry0 << 21;
        /** @var int $carry2 */
        $carry2 = ($s2 + (1 << 20)) >> 21;
        $s3 += $carry2;
        $s2 -= $carry2 << 21;
        /** @var int $carry4 */
        $carry4 = ($s4 + (1 << 20)) >> 21;
        $s5 += $carry4;
        $s4 -= $carry4 << 21;
        /** @var int $carry6 */
        $carry6 = ($s6 + (1 << 20)) >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        /** @var int $carry8 */
        $carry8 = ($s8 + (1 << 20)) >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        /** @var int $carry10 */
        $carry10 = ($s10 + (1 << 20)) >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;
        /** @var int $carry12 */
        $carry12 = ($s12 + (1 << 20)) >> 21;
        $s13 += $carry12;
        $s12 -= $carry12 << 21;
        /** @var int $carry14 */
        $carry14 = ($s14 + (1 << 20)) >> 21;
        $s15 += $carry14;
        $s14 -= $carry14 << 21;
        /** @var int $carry16 */
        $carry16 = ($s16 + (1 << 20)) >> 21;
        $s17 += $carry16;
        $s16 -= $carry16 << 21;
        /** @var int $carry18 */
        $carry18 = ($s18 + (1 << 20)) >> 21;
        $s19 += $carry18;
        $s18 -= $carry18 << 21;
        /** @var int $carry20 */
        $carry20 = ($s20 + (1 << 20)) >> 21;
        $s21 += $carry20;
        $s20 -= $carry20 << 21;
        /** @var int $carry22 */
        $carry22 = ($s22 + (1 << 20)) >> 21;
        $s23 += $carry22;
        $s22 -= $carry22 << 21;

        /** @var int $carry1 */
        $carry1 = ($s1 + (1 << 20)) >> 21;
        $s2 += $carry1;
        $s1 -= $carry1 << 21;
        /** @var int $carry3 */
        $carry3 = ($s3 + (1 << 20)) >> 21;
        $s4 += $carry3;
        $s3 -= $carry3 << 21;
        /** @var int $carry5 */
        $carry5 = ($s5 + (1 << 20)) >> 21;
        $s6 += $carry5;
        $s5 -= $carry5 << 21;
        /** @var int $carry7 */
        $carry7 = ($s7 + (1 << 20)) >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        /** @var int $carry9 */
        $carry9 = ($s9 + (1 << 20)) >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        /** @var int $carry11 */
        $carry11 = ($s11 + (1 << 20)) >> 21;
        $s12 += $carry11;
        $s11 -= $carry11 << 21;
        /** @var int $carry13 */
        $carry13 = ($s13 + (1 << 20)) >> 21;
        $s14 += $carry13;
        $s13 -= $carry13 << 21;
        /** @var int $carry15 */
        $carry15 = ($s15 + (1 << 20)) >> 21;
        $s16 += $carry15;
        $s15 -= $carry15 << 21;
        /** @var int $carry17 */
        $carry17 = ($s17 + (1 << 20)) >> 21;
        $s18 += $carry17;
        $s17 -= $carry17 << 21;
        /** @var int $carry19 */
        $carry19 = ($s19 + (1 << 20)) >> 21;
        $s20 += $carry19;
        $s19 -= $carry19 << 21;
        /** @var int $carry21 */
        $carry21 = ($s21 + (1 << 20)) >> 21;
        $s22 += $carry21;
        $s21 -= $carry21 << 21;

        $s11 += self::mul($s23, 666643, 20);
        $s12 += self::mul($s23, 470296, 19);
        $s13 += self::mul($s23, 654183, 20);
        $s14 -= self::mul($s23, 997805, 20);
        $s15 += self::mul($s23, 136657, 18);
        $s16 -= self::mul($s23, 683901, 20);

        $s10 += self::mul($s22, 666643, 20);
        $s11 += self::mul($s22, 470296, 19);
        $s12 += self::mul($s22, 654183, 20);
        $s13 -= self::mul($s22, 997805, 20);
        $s14 += self::mul($s22, 136657, 18);
        $s15 -= self::mul($s22, 683901, 20);

        $s9  += self::mul($s21,  666643, 20);
        $s10 += self::mul($s21,  470296, 19);
        $s11 += self::mul($s21,  654183, 20);
        $s12 -= self::mul($s21,  997805, 20);
        $s13 += self::mul($s21,  136657, 18);
        $s14 -= self::mul($s21,  683901, 20);

        $s8  += self::mul($s20,  666643, 20);
        $s9  += self::mul($s20,  470296, 19);
        $s10 += self::mul($s20,  654183, 20);
        $s11 -= self::mul($s20,  997805, 20);
        $s12 += self::mul($s20,  136657, 18);
        $s13 -= self::mul($s20,  683901, 20);

        $s7  += self::mul($s19,  666643, 20);
        $s8  += self::mul($s19,  470296, 19);
        $s9  += self::mul($s19,  654183, 20);
        $s10 -= self::mul($s19,  997805, 20);
        $s11 += self::mul($s19,  136657, 18);
        $s12 -= self::mul($s19,  683901, 20);

        $s6  += self::mul($s18,  666643, 20);
        $s7  += self::mul($s18,  470296, 19);
        $s8  += self::mul($s18,  654183, 20);
        $s9  -= self::mul($s18,  997805, 20);
        $s10 += self::mul($s18,  136657, 18);
        $s11 -= self::mul($s18,  683901, 20);

        /** @var int $carry6 */
        $carry6 = ($s6 + (1 << 20)) >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        /** @var int $carry8 */
        $carry8 = ($s8 + (1 << 20)) >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        /** @var int $carry10 */
        $carry10 = ($s10 + (1 << 20)) >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;
        /** @var int $carry12 */
        $carry12 = ($s12 + (1 << 20)) >> 21;
        $s13 += $carry12;
        $s12 -= $carry12 << 21;
        /** @var int $carry14 */
        $carry14 = ($s14 + (1 << 20)) >> 21;
        $s15 += $carry14;
        $s14 -= $carry14 << 21;
        /** @var int $carry16 */
        $carry16 = ($s16 + (1 << 20)) >> 21;
        $s17 += $carry16;
        $s16 -= $carry16 << 21;

        /** @var int $carry7 */
        $carry7 = ($s7 + (1 << 20)) >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        /** @var int $carry9 */
        $carry9 = ($s9 + (1 << 20)) >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        /** @var int $carry11 */
        $carry11 = ($s11 + (1 << 20)) >> 21;
        $s12 += $carry11;
        $s11 -= $carry11 << 21;
        /** @var int $carry13 */
        $carry13 = ($s13 + (1 << 20)) >> 21;
        $s14 += $carry13;
        $s13 -= $carry13 << 21;
        /** @var int $carry15 */
        $carry15 = ($s15 + (1 << 20)) >> 21;
        $s16 += $carry15;
        $s15 -= $carry15 << 21;

        $s5  += self::mul($s17,  666643, 20);
        $s6  += self::mul($s17,  470296, 19);
        $s7  += self::mul($s17,  654183, 20);
        $s8  -= self::mul($s17,  997805, 20);
        $s9  += self::mul($s17,  136657, 18);
        $s10 -= self::mul($s17,  683901, 20);

        $s4 += self::mul($s16,  666643, 20);
        $s5 += self::mul($s16,  470296, 19);
        $s6 += self::mul($s16,  654183, 20);
        $s7 -= self::mul($s16,  997805, 20);
        $s8 += self::mul($s16,  136657, 18);
        $s9 -= self::mul($s16,  683901, 20);

        $s3 += self::mul($s15,  666643, 20);
        $s4 += self::mul($s15,  470296, 19);
        $s5 += self::mul($s15,  654183, 20);
        $s6 -= self::mul($s15,  997805, 20);
        $s7 += self::mul($s15,  136657, 18);
        $s8 -= self::mul($s15,  683901, 20);

        $s2 += self::mul($s14,  666643, 20);
        $s3 += self::mul($s14,  470296, 19);
        $s4 += self::mul($s14,  654183, 20);
        $s5 -= self::mul($s14,  997805, 20);
        $s6 += self::mul($s14,  136657, 18);
        $s7 -= self::mul($s14,  683901, 20);

        $s1 += self::mul($s13,  666643, 20);
        $s2 += self::mul($s13,  470296, 19);
        $s3 += self::mul($s13,  654183, 20);
        $s4 -= self::mul($s13,  997805, 20);
        $s5 += self::mul($s13,  136657, 18);
        $s6 -= self::mul($s13,  683901, 20);

        $s0 += self::mul($s12,  666643, 20);
        $s1 += self::mul($s12,  470296, 19);
        $s2 += self::mul($s12,  654183, 20);
        $s3 -= self::mul($s12,  997805, 20);
        $s4 += self::mul($s12,  136657, 18);
        $s5 -= self::mul($s12,  683901, 20);
        $s12 = 0;

        /** @var int $carry0 */
        $carry0 = ($s0 + (1 << 20)) >> 21;
        $s1 += $carry0;
        $s0 -= $carry0 << 21;
        /** @var int $carry2 */
        $carry2 = ($s2 + (1 << 20)) >> 21;
        $s3 += $carry2;
        $s2 -= $carry2 << 21;
        /** @var int $carry4 */
        $carry4 = ($s4 + (1 << 20)) >> 21;
        $s5 += $carry4;
        $s4 -= $carry4 << 21;
        /** @var int $carry6 */
        $carry6 = ($s6 + (1 << 20)) >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        /** @var int $carry8 */
        $carry8 = ($s8 + (1 << 20)) >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        /** @var int $carry10 */
        $carry10 = ($s10 + (1 << 20)) >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;

        /** @var int $carry1 */
        $carry1 = ($s1 + (1 << 20)) >> 21;
        $s2 += $carry1;
        $s1 -= $carry1 << 21;
        /** @var int $carry3 */
        $carry3 = ($s3 + (1 << 20)) >> 21;
        $s4 += $carry3;
        $s3 -= $carry3 << 21;
        /** @var int $carry5 */
        $carry5 = ($s5 + (1 << 20)) >> 21;
        $s6 += $carry5;
        $s5 -= $carry5 << 21;
        /** @var int $carry7 */
        $carry7 = ($s7 + (1 << 20)) >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        /** @var int $carry9 */
        $carry9 = ($s9 + (1 << 20)) >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        /** @var int $carry11 */
        $carry11 = ($s11 + (1 << 20)) >> 21;
        $s12 += $carry11;
        $s11 -= $carry11 << 21;

        $s0 += self::mul($s12,  666643, 20);
        $s1 += self::mul($s12,  470296, 19);
        $s2 += self::mul($s12,  654183, 20);
        $s3 -= self::mul($s12,  997805, 20);
        $s4 += self::mul($s12,  136657, 18);
        $s5 -= self::mul($s12,  683901, 20);
        $s12 = 0;

        /** @var int $carry0 */
        $carry0 = $s0 >> 21;
        $s1 += $carry0;
        $s0 -= $carry0 << 21;
        /** @var int $carry1 */
        $carry1 = $s1 >> 21;
        $s2 += $carry1;
        $s1 -= $carry1 << 21;
        /** @var int $carry2 */
        $carry2 = $s2 >> 21;
        $s3 += $carry2;
        $s2 -= $carry2 << 21;
        /** @var int $carry3 */
        $carry3 = $s3 >> 21;
        $s4 += $carry3;
        $s3 -= $carry3 << 21;
        /** @var int $carry4 */
        $carry4 = $s4 >> 21;
        $s5 += $carry4;
        $s4 -= $carry4 << 21;
        /** @var int $carry5 */
        $carry5 = $s5 >> 21;
        $s6 += $carry5;
        $s5 -= $carry5 << 21;
        /** @var int $carry6 */
        $carry6 = $s6 >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        /** @var int $carry7 */
        $carry7 = $s7 >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        /** @var int $carry8 */
        $carry8 = $s8 >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        /** @var int $carry9 */
        $carry9 = $s9 >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        /** @var int $carry10 */
        $carry10 = $s10 >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;
        /** @var int $carry11 */
        $carry11 = $s11 >> 21;
        $s12 += $carry11;
        $s11 -= $carry11 << 21;

        $s0 += self::mul($s12,  666643, 20);
        $s1 += self::mul($s12,  470296, 19);
        $s2 += self::mul($s12,  654183, 20);
        $s3 -= self::mul($s12,  997805, 20);
        $s4 += self::mul($s12,  136657, 18);
        $s5 -= self::mul($s12,  683901, 20);

        /** @var int $carry0 */
        $carry0 = $s0 >> 21;
        $s1 += $carry0;
        $s0 -= $carry0 << 21;
        /** @var int $carry1 */
        $carry1 = $s1 >> 21;
        $s2 += $carry1;
        $s1 -= $carry1 << 21;
        /** @var int $carry2 */
        $carry2 = $s2 >> 21;
        $s3 += $carry2;
        $s2 -= $carry2 << 21;
        /** @var int $carry3 */
        $carry3 = $s3 >> 21;
        $s4 += $carry3;
        $s3 -= $carry3 << 21;
        /** @var int $carry4 */
        $carry4 = $s4 >> 21;
        $s5 += $carry4;
        $s4 -= $carry4 << 21;
        /** @var int $carry5 */
        $carry5 = $s5 >> 21;
        $s6 += $carry5;
        $s5 -= $carry5 << 21;
        /** @var int $carry6 */
        $carry6 = $s6 >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        /** @var int $carry7 */
        $carry7 = $s7 >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        /** @var int $carry8 */
        $carry8 = $s8 >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        /** @var int $carry9 */
        $carry9 = $s9 >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        /** @var int $carry10 */
        $carry10 = $s10 >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;

        /**
         * @var array<int, int>
         */
        $arr = array(
            (int) (0xff & ($s0 >> 0)),
            (int) (0xff & ($s0 >> 8)),
            (int) (0xff & (($s0 >> 16) | $s1 << 5)),
            (int) (0xff & ($s1 >> 3)),
            (int) (0xff & ($s1 >> 11)),
            (int) (0xff & (($s1 >> 19) | $s2 << 2)),
            (int) (0xff & ($s2 >> 6)),
            (int) (0xff & (($s2 >> 14) | $s3 << 7)),
            (int) (0xff & ($s3 >> 1)),
            (int) (0xff & ($s3 >> 9)),
            (int) (0xff & (($s3 >> 17) | $s4 << 4)),
            (int) (0xff & ($s4 >> 4)),
            (int) (0xff & ($s4 >> 12)),
            (int) (0xff & (($s4 >> 20) | $s5 << 1)),
            (int) (0xff & ($s5 >> 7)),
            (int) (0xff & (($s5 >> 15) | $s6 << 6)),
            (int) (0xff & ($s6 >> 2)),
            (int) (0xff & ($s6 >> 10)),
            (int) (0xff & (($s6 >> 18) | $s7 << 3)),
            (int) (0xff & ($s7 >> 5)),
            (int) (0xff & ($s7 >> 13)),
            (int) (0xff & ($s8 >> 0)),
            (int) (0xff & ($s8 >> 8)),
            (int) (0xff & (($s8 >> 16) | $s9 << 5)),
            (int) (0xff & ($s9 >> 3)),
            (int) (0xff & ($s9 >> 11)),
            (int) (0xff & (($s9 >> 19) | $s10 << 2)),
            (int) (0xff & ($s10 >> 6)),
            (int) (0xff & (($s10 >> 14) | $s11 << 7)),
            (int) (0xff & ($s11 >> 1)),
            (int) (0xff & ($s11 >> 9)),
            0xff & ($s11 >> 17)
        );
        return self::intArrayToString($arr);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $s
     * @return string
     * @throws TypeError
     */
    public static function sc_reduce($s)
    {
        /** @var int $s0 */
        $s0 = 2097151 & self::load_3(self::substr($s, 0, 3));
        /** @var int $s1 */
        $s1 = 2097151 & (self::load_4(self::substr($s, 2, 4)) >> 5);
        /** @var int $s2 */
        $s2 = 2097151 & (self::load_3(self::substr($s, 5, 3)) >> 2);
        /** @var int $s3 */
        $s3 = 2097151 & (self::load_4(self::substr($s, 7, 4)) >> 7);
        /** @var int $s4 */
        $s4 = 2097151 & (self::load_4(self::substr($s, 10, 4)) >> 4);
        /** @var int $s5 */
        $s5 = 2097151 & (self::load_3(self::substr($s, 13, 3)) >> 1);
        /** @var int $s6 */
        $s6 = 2097151 & (self::load_4(self::substr($s, 15, 4)) >> 6);
        /** @var int $s7 */
        $s7 = 2097151 & (self::load_3(self::substr($s, 18, 4)) >> 3);
        /** @var int $s8 */
        $s8 = 2097151 & self::load_3(self::substr($s, 21, 3));
        /** @var int $s9 */
        $s9 = 2097151 & (self::load_4(self::substr($s, 23, 4)) >> 5);
        /** @var int $s10 */
        $s10 = 2097151 & (self::load_3(self::substr($s, 26, 3)) >> 2);
        /** @var int $s11 */
        $s11 = 2097151 & (self::load_4(self::substr($s, 28, 4)) >> 7);
        /** @var int $s12 */
        $s12 = 2097151 & (self::load_4(self::substr($s, 31, 4)) >> 4);
        /** @var int $s13 */
        $s13 = 2097151 & (self::load_3(self::substr($s, 34, 3)) >> 1);
        /** @var int $s14 */
        $s14 = 2097151 & (self::load_4(self::substr($s, 36, 4)) >> 6);
        /** @var int $s15 */
        $s15 = 2097151 & (self::load_3(self::substr($s, 39, 4)) >> 3);
        /** @var int $s16 */
        $s16 = 2097151 & self::load_3(self::substr($s, 42, 3));
        /** @var int $s17 */
        $s17 = 2097151 & (self::load_4(self::substr($s, 44, 4)) >> 5);
        /** @var int $s18 */
        $s18 = 2097151 & (self::load_3(self::substr($s, 47, 3)) >> 2);
        /** @var int $s19 */
        $s19 = 2097151 & (self::load_4(self::substr($s, 49, 4)) >> 7);
        /** @var int $s20 */
        $s20 = 2097151 & (self::load_4(self::substr($s, 52, 4)) >> 4);
        /** @var int $s21 */
        $s21 = 2097151 & (self::load_3(self::substr($s, 55, 3)) >> 1);
        /** @var int $s22 */
        $s22 = 2097151 & (self::load_4(self::substr($s, 57, 4)) >> 6);
        /** @var int $s23 */
        $s23 = (self::load_4(self::substr($s, 60, 4)) >> 3);

        $s11 += self::mul($s23,  666643, 20);
        $s12 += self::mul($s23,  470296, 19);
        $s13 += self::mul($s23,  654183, 20);
        $s14 -= self::mul($s23,  997805, 20);
        $s15 += self::mul($s23,  136657, 18);
        $s16 -= self::mul($s23,  683901, 20);

        $s10 += self::mul($s22,  666643, 20);
        $s11 += self::mul($s22,  470296, 19);
        $s12 += self::mul($s22,  654183, 20);
        $s13 -= self::mul($s22,  997805, 20);
        $s14 += self::mul($s22,  136657, 18);
        $s15 -= self::mul($s22,  683901, 20);

        $s9  += self::mul($s21,  666643, 20);
        $s10 += self::mul($s21,  470296, 19);
        $s11 += self::mul($s21,  654183, 20);
        $s12 -= self::mul($s21,  997805, 20);
        $s13 += self::mul($s21,  136657, 18);
        $s14 -= self::mul($s21,  683901, 20);

        $s8  += self::mul($s20,  666643, 20);
        $s9  += self::mul($s20,  470296, 19);
        $s10 += self::mul($s20,  654183, 20);
        $s11 -= self::mul($s20,  997805, 20);
        $s12 += self::mul($s20,  136657, 18);
        $s13 -= self::mul($s20,  683901, 20);

        $s7  += self::mul($s19,  666643, 20);
        $s8  += self::mul($s19,  470296, 19);
        $s9  += self::mul($s19,  654183, 20);
        $s10 -= self::mul($s19,  997805, 20);
        $s11 += self::mul($s19,  136657, 18);
        $s12 -= self::mul($s19,  683901, 20);

        $s6  += self::mul($s18,  666643, 20);
        $s7  += self::mul($s18,  470296, 19);
        $s8  += self::mul($s18,  654183, 20);
        $s9  -= self::mul($s18,  997805, 20);
        $s10 += self::mul($s18,  136657, 18);
        $s11 -= self::mul($s18,  683901, 20);

        /** @var int $carry6 */
        $carry6 = ($s6 + (1 << 20)) >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        /** @var int $carry8 */
        $carry8 = ($s8 + (1 << 20)) >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        /** @var int $carry10 */
        $carry10 = ($s10 + (1 << 20)) >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;
        /** @var int $carry12 */
        $carry12 = ($s12 + (1 << 20)) >> 21;
        $s13 += $carry12;
        $s12 -= $carry12 << 21;
        /** @var int $carry14 */
        $carry14 = ($s14 + (1 << 20)) >> 21;
        $s15 += $carry14;
        $s14 -= $carry14 << 21;
        /** @var int $carry16 */
        $carry16 = ($s16 + (1 << 20)) >> 21;
        $s17 += $carry16;
        $s16 -= $carry16 << 21;

        /** @var int $carry7 */
        $carry7 = ($s7 + (1 << 20)) >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        /** @var int $carry9 */
        $carry9 = ($s9 + (1 << 20)) >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        /** @var int $carry11 */
        $carry11 = ($s11 + (1 << 20)) >> 21;
        $s12 += $carry11;
        $s11 -= $carry11 << 21;
        /** @var int $carry13 */
        $carry13 = ($s13 + (1 << 20)) >> 21;
        $s14 += $carry13;
        $s13 -= $carry13 << 21;
        /** @var int $carry15 */
        $carry15 = ($s15 + (1 << 20)) >> 21;
        $s16 += $carry15;
        $s15 -= $carry15 << 21;

        $s5  += self::mul($s17,  666643, 20);
        $s6  += self::mul($s17,  470296, 19);
        $s7  += self::mul($s17,  654183, 20);
        $s8  -= self::mul($s17,  997805, 20);
        $s9  += self::mul($s17,  136657, 18);
        $s10 -= self::mul($s17,  683901, 20);

        $s4 += self::mul($s16,  666643, 20);
        $s5 += self::mul($s16,  470296, 19);
        $s6 += self::mul($s16,  654183, 20);
        $s7 -= self::mul($s16,  997805, 20);
        $s8 += self::mul($s16,  136657, 18);
        $s9 -= self::mul($s16,  683901, 20);

        $s3 += self::mul($s15,  666643, 20);
        $s4 += self::mul($s15,  470296, 19);
        $s5 += self::mul($s15,  654183, 20);
        $s6 -= self::mul($s15,  997805, 20);
        $s7 += self::mul($s15,  136657, 18);
        $s8 -= self::mul($s15,  683901, 20);

        $s2 += self::mul($s14,  666643, 20);
        $s3 += self::mul($s14,  470296, 19);
        $s4 += self::mul($s14,  654183, 20);
        $s5 -= self::mul($s14,  997805, 20);
        $s6 += self::mul($s14,  136657, 18);
        $s7 -= self::mul($s14,  683901, 20);

        $s1 += self::mul($s13,  666643, 20);
        $s2 += self::mul($s13,  470296, 19);
        $s3 += self::mul($s13,  654183, 20);
        $s4 -= self::mul($s13,  997805, 20);
        $s5 += self::mul($s13,  136657, 18);
        $s6 -= self::mul($s13,  683901, 20);

        $s0 += self::mul($s12,  666643, 20);
        $s1 += self::mul($s12,  470296, 19);
        $s2 += self::mul($s12,  654183, 20);
        $s3 -= self::mul($s12,  997805, 20);
        $s4 += self::mul($s12,  136657, 18);
        $s5 -= self::mul($s12,  683901, 20);
        $s12 = 0;

        /** @var int $carry0 */
        $carry0 = ($s0 + (1 << 20)) >> 21;
        $s1 += $carry0;
        $s0 -= $carry0 << 21;
        /** @var int $carry2 */
        $carry2 = ($s2 + (1 << 20)) >> 21;
        $s3 += $carry2;
        $s2 -= $carry2 << 21;
        /** @var int $carry4 */
        $carry4 = ($s4 + (1 << 20)) >> 21;
        $s5 += $carry4;
        $s4 -= $carry4 << 21;
        /** @var int $carry6 */
        $carry6 = ($s6 + (1 << 20)) >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        /** @var int $carry8 */
        $carry8 = ($s8 + (1 << 20)) >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        /** @var int $carry10 */
        $carry10 = ($s10 + (1 << 20)) >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;

        /** @var int $carry1 */
        $carry1 = ($s1 + (1 << 20)) >> 21;
        $s2 += $carry1;
        $s1 -= $carry1 << 21;
        /** @var int $carry3 */
        $carry3 = ($s3 + (1 << 20)) >> 21;
        $s4 += $carry3;
        $s3 -= $carry3 << 21;
        /** @var int $carry5 */
        $carry5 = ($s5 + (1 << 20)) >> 21;
        $s6 += $carry5;
        $s5 -= $carry5 << 21;
        /** @var int $carry7 */
        $carry7 = ($s7 + (1 << 20)) >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        /** @var int $carry9 */
        $carry9 = ($s9 + (1 << 20)) >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        /** @var int $carry11 */
        $carry11 = ($s11 + (1 << 20)) >> 21;
        $s12 += $carry11;
        $s11 -= $carry11 << 21;

        $s0 += self::mul($s12,  666643, 20);
        $s1 += self::mul($s12,  470296, 19);
        $s2 += self::mul($s12,  654183, 20);
        $s3 -= self::mul($s12,  997805, 20);
        $s4 += self::mul($s12,  136657, 18);
        $s5 -= self::mul($s12,  683901, 20);
        $s12 = 0;

        /** @var int $carry0 */
        $carry0 = $s0 >> 21;
        $s1 += $carry0;
        $s0 -= $carry0 << 21;
        /** @var int $carry1 */
        $carry1 = $s1 >> 21;
        $s2 += $carry1;
        $s1 -= $carry1 << 21;
        /** @var int $carry2 */
        $carry2 = $s2 >> 21;
        $s3 += $carry2;
        $s2 -= $carry2 << 21;
        /** @var int $carry3 */
        $carry3 = $s3 >> 21;
        $s4 += $carry3;
        $s3 -= $carry3 << 21;
        /** @var int $carry4 */
        $carry4 = $s4 >> 21;
        $s5 += $carry4;
        $s4 -= $carry4 << 21;
        /** @var int $carry5 */
        $carry5 = $s5 >> 21;
        $s6 += $carry5;
        $s5 -= $carry5 << 21;
        /** @var int $carry6 */
        $carry6 = $s6 >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        /** @var int $carry7 */
        $carry7 = $s7 >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        /** @var int $carry8 */
        $carry8 = $s8 >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        /** @var int $carry9 */
        $carry9 = $s9 >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        /** @var int $carry10 */
        $carry10 = $s10 >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;
        /** @var int $carry11 */
        $carry11 = $s11 >> 21;
        $s12 += $carry11;
        $s11 -= $carry11 << 21;

        $s0 += self::mul($s12,  666643, 20);
        $s1 += self::mul($s12,  470296, 19);
        $s2 += self::mul($s12,  654183, 20);
        $s3 -= self::mul($s12,  997805, 20);
        $s4 += self::mul($s12,  136657, 18);
        $s5 -= self::mul($s12,  683901, 20);

        /** @var int $carry0 */
        $carry0 = $s0 >> 21;
        $s1 += $carry0;
        $s0 -= $carry0 << 21;
        /** @var int $carry1 */
        $carry1 = $s1 >> 21;
        $s2 += $carry1;
        $s1 -= $carry1 << 21;
        /** @var int $carry2 */
        $carry2 = $s2 >> 21;
        $s3 += $carry2;
        $s2 -= $carry2 << 21;
        /** @var int $carry3 */
        $carry3 = $s3 >> 21;
        $s4 += $carry3;
        $s3 -= $carry3 << 21;
        /** @var int $carry4 */
        $carry4 = $s4 >> 21;
        $s5 += $carry4;
        $s4 -= $carry4 << 21;
        /** @var int $carry5 */
        $carry5 = $s5 >> 21;
        $s6 += $carry5;
        $s5 -= $carry5 << 21;
        /** @var int $carry6 */
        $carry6 = $s6 >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        /** @var int $carry7 */
        $carry7 = $s7 >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        /** @var int $carry8 */
        $carry8 = $s8 >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        /** @var int $carry9 */
        $carry9 = $s9 >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        /** @var int $carry10 */
        $carry10 = $s10 >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;

        /**
         * @var array<int, int>
         */
        $arr = array(
            (int) ($s0 >> 0),
            (int) ($s0 >> 8),
            (int) (($s0 >> 16) | $s1 << 5),
            (int) ($s1 >> 3),
            (int) ($s1 >> 11),
            (int) (($s1 >> 19) | $s2 << 2),
            (int) ($s2 >> 6),
            (int) (($s2 >> 14) | $s3 << 7),
            (int) ($s3 >> 1),
            (int) ($s3 >> 9),
            (int) (($s3 >> 17) | $s4 << 4),
            (int) ($s4 >> 4),
            (int) ($s4 >> 12),
            (int) (($s4 >> 20) | $s5 << 1),
            (int) ($s5 >> 7),
            (int) (($s5 >> 15) | $s6 << 6),
            (int) ($s6 >> 2),
            (int) ($s6 >> 10),
            (int) (($s6 >> 18) | $s7 << 3),
            (int) ($s7 >> 5),
            (int) ($s7 >> 13),
            (int) ($s8 >> 0),
            (int) ($s8 >> 8),
            (int) (($s8 >> 16) | $s9 << 5),
            (int) ($s9 >> 3),
            (int) ($s9 >> 11),
            (int) (($s9 >> 19) | $s10 << 2),
            (int) ($s10 >> 6),
            (int) (($s10 >> 14) | $s11 << 7),
            (int) ($s11 >> 1),
            (int) ($s11 >> 9),
            (int) $s11 >> 17
        );
        return self::intArrayToString($arr);
    }

    /**
     * multiply by the order of the main subgroup l = 2^252+27742317777372353535851937790883648493
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P3
     */
    public static function ge_mul_l(ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A)
    {
        /** @var array<int, int> $aslide */
        $aslide = array(
            13, 0, 0, 0, 0, -1, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0,
            0, 0, 0, -3, 0, 0, 0, 0, -13, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 3, 0,
            0, 0, 0, -13, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0,
            0, 0, 11, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -1,
            0, 0, 0, 0, 3, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0,
            0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 7, 0, 0, 0, 0, 5, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
        );

        /** @var array<int, ParagonIE_Sodium_Core_Curve25519_Ge_Cached> $Ai size 8 */
        $Ai = array();

        # ge_p3_to_cached(&Ai[0], A);
        $Ai[0] = self::ge_p3_to_cached($A);
        # ge_p3_dbl(&t, A);
        $t = self::ge_p3_dbl($A);
        # ge_p1p1_to_p3(&A2, &t);
        $A2 = self::ge_p1p1_to_p3($t);

        for ($i = 1; $i < 8; ++$i) {
            # ge_add(&t, &A2, &Ai[0]);
            $t = self::ge_add($A2, $Ai[$i - 1]);
            # ge_p1p1_to_p3(&u, &t);
            $u = self::ge_p1p1_to_p3($t);
            # ge_p3_to_cached(&Ai[i], &u);
            $Ai[$i] = self::ge_p3_to_cached($u);
        }

        $r = self::ge_p3_0();
        for ($i = 252; $i >= 0; --$i) {
            $t = self::ge_p3_dbl($r);
            if ($aslide[$i] > 0) {
                # ge_p1p1_to_p3(&u, &t);
                $u = self::ge_p1p1_to_p3($t);
                # ge_add(&t, &u, &Ai[aslide[i] / 2]);
                $t = self::ge_add($u, $Ai[(int)($aslide[$i] / 2)]);
            } elseif ($aslide[$i] < 0) {
                # ge_p1p1_to_p3(&u, &t);
                $u = self::ge_p1p1_to_p3($t);
                # ge_sub(&t, &u, &Ai[(-aslide[i]) / 2]);
                $t = self::ge_sub($u, $Ai[(int)(-$aslide[$i] / 2)]);
            }
        }

        # ge_p1p1_to_p3(r, &t);
        return self::ge_p1p1_to_p3($t);
    }

    /**
     * @param string $a
     * @param string $b
     * @return string
     */
    public static function sc25519_mul($a, $b)
    {
        //    int64_t a0  = 2097151 & load_3(a);
        //    int64_t a1  = 2097151 & (load_4(a + 2) >> 5);
        //    int64_t a2  = 2097151 & (load_3(a + 5) >> 2);
        //    int64_t a3  = 2097151 & (load_4(a + 7) >> 7);
        //    int64_t a4  = 2097151 & (load_4(a + 10) >> 4);
        //    int64_t a5  = 2097151 & (load_3(a + 13) >> 1);
        //    int64_t a6  = 2097151 & (load_4(a + 15) >> 6);
        //    int64_t a7  = 2097151 & (load_3(a + 18) >> 3);
        //    int64_t a8  = 2097151 & load_3(a + 21);
        //    int64_t a9  = 2097151 & (load_4(a + 23) >> 5);
        //    int64_t a10 = 2097151 & (load_3(a + 26) >> 2);
        //    int64_t a11 = (load_4(a + 28) >> 7);
        $a0  = 2097151 &  self::load_3(self::substr($a, 0, 3));
        $a1  = 2097151 & (self::load_4(self::substr($a, 2, 4)) >> 5);
        $a2  = 2097151 & (self::load_3(self::substr($a, 5, 3)) >> 2);
        $a3  = 2097151 & (self::load_4(self::substr($a, 7, 4)) >> 7);
        $a4  = 2097151 & (self::load_4(self::substr($a, 10, 4)) >> 4);
        $a5  = 2097151 & (self::load_3(self::substr($a, 13, 3)) >> 1);
        $a6  = 2097151 & (self::load_4(self::substr($a, 15, 4)) >> 6);
        $a7  = 2097151 & (self::load_3(self::substr($a, 18, 3)) >> 3);
        $a8  = 2097151 &  self::load_3(self::substr($a, 21, 3));
        $a9  = 2097151 & (self::load_4(self::substr($a, 23, 4)) >> 5);
        $a10 = 2097151 & (self::load_3(self::substr($a, 26, 3)) >> 2);
        $a11 = (self::load_4(self::substr($a, 28, 4)) >> 7);

        //    int64_t b0  = 2097151 & load_3(b);
        //    int64_t b1  = 2097151 & (load_4(b + 2) >> 5);
        //    int64_t b2  = 2097151 & (load_3(b + 5) >> 2);
        //    int64_t b3  = 2097151 & (load_4(b + 7) >> 7);
        //    int64_t b4  = 2097151 & (load_4(b + 10) >> 4);
        //    int64_t b5  = 2097151 & (load_3(b + 13) >> 1);
        //    int64_t b6  = 2097151 & (load_4(b + 15) >> 6);
        //    int64_t b7  = 2097151 & (load_3(b + 18) >> 3);
        //    int64_t b8  = 2097151 & load_3(b + 21);
        //    int64_t b9  = 2097151 & (load_4(b + 23) >> 5);
        //    int64_t b10 = 2097151 & (load_3(b + 26) >> 2);
        //    int64_t b11 = (load_4(b + 28) >> 7);
        $b0  = 2097151 &  self::load_3(self::substr($b, 0, 3));
        $b1  = 2097151 & (self::load_4(self::substr($b, 2, 4)) >> 5);
        $b2  = 2097151 & (self::load_3(self::substr($b, 5, 3)) >> 2);
        $b3  = 2097151 & (self::load_4(self::substr($b, 7, 4)) >> 7);
        $b4  = 2097151 & (self::load_4(self::substr($b, 10, 4)) >> 4);
        $b5  = 2097151 & (self::load_3(self::substr($b, 13, 3)) >> 1);
        $b6  = 2097151 & (self::load_4(self::substr($b, 15, 4)) >> 6);
        $b7  = 2097151 & (self::load_3(self::substr($b, 18, 3)) >> 3);
        $b8  = 2097151 &  self::load_3(self::substr($b, 21, 3));
        $b9  = 2097151 & (self::load_4(self::substr($b, 23, 4)) >> 5);
        $b10 = 2097151 & (self::load_3(self::substr($b, 26, 3)) >> 2);
        $b11 = (self::load_4(self::substr($b, 28, 4)) >> 7);

        //    s0 = a0 * b0;
        //    s1 = a0 * b1 + a1 * b0;
        //    s2 = a0 * b2 + a1 * b1 + a2 * b0;
        //    s3 = a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0;
        //    s4 = a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0;
        //    s5 = a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0;
        //    s6 = a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0;
        //    s7 = a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 +
        //        a6 * b1 + a7 * b0;
        //    s8 = a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 +
        //        a6 * b2 + a7 * b1 + a8 * b0;
        //    s9 = a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 +
        //        a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0;
        //    s10 = a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 +
        //        a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0;
        //    s11 = a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 +
        //        a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0;
        //    s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 +
        //        a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1;
        //    s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 +
        //        a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2;
        //    s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 +
        //        a9 * b5 + a10 * b4 + a11 * b3;
        //    s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 +
        //        a10 * b5 + a11 * b4;
        //    s16 =
        //        a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5;
        //    s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6;
        //    s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7;
        //    s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8;
        //    s20 = a9 * b11 + a10 * b10 + a11 * b9;
        //    s21 = a10 * b11 + a11 * b10;
        //    s22 = a11 * b11;
        //    s23 = 0;
        $s0 = self::mul($a0, $b0, 22);
        $s1 = self::mul($a0, $b1, 22) + self::mul($a1, $b0, 22);
        $s2 = self::mul($a0, $b2, 22) + self::mul($a1, $b1, 22) + self::mul($a2, $b0, 22);
        $s3 = self::mul($a0, $b3, 22) + self::mul($a1, $b2, 22) + self::mul($a2, $b1, 22) + self::mul($a3, $b0, 22);
        $s4 = self::mul($a0, $b4, 22) + self::mul($a1, $b3, 22) + self::mul($a2, $b2, 22) + self::mul($a3, $b1, 22) +
            self::mul($a4, $b0, 22);
        $s5 = self::mul($a0, $b5, 22) + self::mul($a1, $b4, 22) + self::mul($a2, $b3, 22) + self::mul($a3, $b2, 22) +
            self::mul($a4, $b1, 22) + self::mul($a5, $b0, 22);
        $s6 = self::mul($a0, $b6, 22) + self::mul($a1, $b5, 22) + self::mul($a2, $b4, 22) + self::mul($a3, $b3, 22) +
            self::mul($a4, $b2, 22) + self::mul($a5, $b1, 22) + self::mul($a6, $b0, 22);
        $s7 = self::mul($a0, $b7, 22) + self::mul($a1, $b6, 22) + self::mul($a2, $b5, 22) + self::mul($a3, $b4, 22) +
            self::mul($a4, $b3, 22) + self::mul($a5, $b2, 22) + self::mul($a6, $b1, 22) + self::mul($a7, $b0, 22);
        $s8 = self::mul($a0, $b8, 22) + self::mul($a1, $b7, 22) + self::mul($a2, $b6, 22) + self::mul($a3, $b5, 22) +
            self::mul($a4, $b4, 22) + self::mul($a5, $b3, 22) + self::mul($a6, $b2, 22) + self::mul($a7, $b1, 22) +
            self::mul($a8, $b0, 22);
        $s9 = self::mul($a0, $b9, 22) + self::mul($a1, $b8, 22) + self::mul($a2, $b7, 22) + self::mul($a3, $b6, 22) +
            self::mul($a4, $b5, 22) + self::mul($a5, $b4, 22) + self::mul($a6, $b3, 22) + self::mul($a7, $b2, 22) +
            self::mul($a8, $b1, 22) + self::mul($a9, $b0, 22);
        $s10 = self::mul($a0, $b10, 22) + self::mul($a1, $b9, 22) + self::mul($a2, $b8, 22) + self::mul($a3, $b7, 22) +
            self::mul($a4, $b6, 22) + self::mul($a5, $b5, 22) + self::mul($a6, $b4, 22) + self::mul($a7, $b3, 22) +
            self::mul($a8, $b2, 22) + self::mul($a9, $b1, 22) + self::mul($a10, $b0, 22);
        $s11 = self::mul($a0, $b11, 22) + self::mul($a1, $b10, 22) + self::mul($a2, $b9, 22) + self::mul($a3, $b8, 22) +
            self::mul($a4, $b7, 22) + self::mul($a5, $b6, 22) + self::mul($a6, $b5, 22) + self::mul($a7, $b4, 22) +
            self::mul($a8, $b3, 22) + self::mul($a9, $b2, 22) + self::mul($a10, $b1, 22) + self::mul($a11, $b0, 22);
        $s12 = self::mul($a1, $b11, 22) + self::mul($a2, $b10, 22) + self::mul($a3, $b9, 22) + self::mul($a4, $b8, 22) +
            self::mul($a5, $b7, 22) + self::mul($a6, $b6, 22) + self::mul($a7, $b5, 22) + self::mul($a8, $b4, 22) +
            self::mul($a9, $b3, 22) + self::mul($a10, $b2, 22) + self::mul($a11, $b1, 22);
        $s13 = self::mul($a2, $b11, 22) + self::mul($a3, $b10, 22) + self::mul($a4, $b9, 22) + self::mul($a5, $b8, 22) +
            self::mul($a6, $b7, 22) + self::mul($a7, $b6, 22) + self::mul($a8, $b5, 22) + self::mul($a9, $b4, 22) +
            self::mul($a10, $b3, 22) + self::mul($a11, $b2, 22);
        $s14 = self::mul($a3, $b11, 22) + self::mul($a4, $b10, 22) + self::mul($a5, $b9, 22) + self::mul($a6, $b8, 22) +
            self::mul($a7, $b7, 22) + self::mul($a8, $b6, 22) + self::mul($a9, $b5, 22) + self::mul($a10, $b4, 22) +
            self::mul($a11, $b3, 22);
        $s15 = self::mul($a4, $b11, 22) + self::mul($a5, $b10, 22) + self::mul($a6, $b9, 22) + self::mul($a7, $b8, 22) +
            self::mul($a8, $b7, 22) + self::mul($a9, $b6, 22) + self::mul($a10, $b5, 22) + self::mul($a11, $b4, 22);
        $s16 =
            self::mul($a5, $b11, 22) + self::mul($a6, $b10, 22) + self::mul($a7, $b9, 22) + self::mul($a8, $b8, 22) +
            self::mul($a9, $b7, 22) + self::mul($a10, $b6, 22) + self::mul($a11, $b5, 22);
        $s17 = self::mul($a6, $b11, 22) + self::mul($a7, $b10, 22) + self::mul($a8, $b9, 22) + self::mul($a9, $b8, 22) +
            self::mul($a10, $b7, 22) + self::mul($a11, $b6, 22);
        $s18 = self::mul($a7, $b11, 22) + self::mul($a8, $b10, 22) + self::mul($a9, $b9, 22) + self::mul($a10, $b8, 22)
            + self::mul($a11, $b7, 22);
        $s19 = self::mul($a8, $b11, 22) + self::mul($a9, $b10, 22) + self::mul($a10, $b9, 22) +
            self::mul($a11, $b8, 22);
        $s20 = self::mul($a9, $b11, 22) + self::mul($a10, $b10, 22) + self::mul($a11, $b9, 22);
        $s21 = self::mul($a10, $b11, 22) + self::mul($a11, $b10, 22);
        $s22 = self::mul($a11, $b11, 22);
        $s23 = 0;

        //    carry0 = (s0 + (int64_t) (1L << 20)) >> 21;
        //    s1 += carry0;
        //    s0 -= carry0 * ((uint64_t) 1L << 21);
        $carry0 = ($s0 + (1 << 20)) >> 21;
        $s1 += $carry0;
        $s0 -= $carry0 << 21;
        //    carry2 = (s2 + (int64_t) (1L << 20)) >> 21;
        //    s3 += carry2;
        //    s2 -= carry2 * ((uint64_t) 1L << 21);
        $carry2 = ($s2 + (1 << 20)) >> 21;
        $s3 += $carry2;
        $s2 -= $carry2 << 21;
        //    carry4 = (s4 + (int64_t) (1L << 20)) >> 21;
        //    s5 += carry4;
        //    s4 -= carry4 * ((uint64_t) 1L << 21);
        $carry4 = ($s4 + (1 << 20)) >> 21;
        $s5 += $carry4;
        $s4 -= $carry4 << 21;
        //    carry6 = (s6 + (int64_t) (1L << 20)) >> 21;
        //    s7 += carry6;
        //    s6 -= carry6 * ((uint64_t) 1L << 21);
        $carry6 = ($s6 + (1 << 20)) >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        //    carry8 = (s8 + (int64_t) (1L << 20)) >> 21;
        //    s9 += carry8;
        //    s8 -= carry8 * ((uint64_t) 1L << 21);
        $carry8 = ($s8 + (1 << 20)) >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        //    carry10 = (s10 + (int64_t) (1L << 20)) >> 21;
        //    s11 += carry10;
        //    s10 -= carry10 * ((uint64_t) 1L << 21);
        $carry10 = ($s10 + (1 << 20)) >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;
        //    carry12 = (s12 + (int64_t) (1L << 20)) >> 21;
        //    s13 += carry12;
        //    s12 -= carry12 * ((uint64_t) 1L << 21);
        $carry12 = ($s12 + (1 << 20)) >> 21;
        $s13 += $carry12;
        $s12 -= $carry12 << 21;
        //    carry14 = (s14 + (int64_t) (1L << 20)) >> 21;
        //    s15 += carry14;
        //    s14 -= carry14 * ((uint64_t) 1L << 21);
        $carry14 = ($s14 + (1 << 20)) >> 21;
        $s15 += $carry14;
        $s14 -= $carry14 << 21;
        //    carry16 = (s16 + (int64_t) (1L << 20)) >> 21;
        //    s17 += carry16;
        //    s16 -= carry16 * ((uint64_t) 1L << 21);
        $carry16 = ($s16 + (1 << 20)) >> 21;
        $s17 += $carry16;
        $s16 -= $carry16 << 21;
        //    carry18 = (s18 + (int64_t) (1L << 20)) >> 21;
        //    s19 += carry18;
        //    s18 -= carry18 * ((uint64_t) 1L << 21);
        $carry18 = ($s18 + (1 << 20)) >> 21;
        $s19 += $carry18;
        $s18 -= $carry18 << 21;
        //    carry20 = (s20 + (int64_t) (1L << 20)) >> 21;
        //    s21 += carry20;
        //    s20 -= carry20 * ((uint64_t) 1L << 21);
        $carry20 = ($s20 + (1 << 20)) >> 21;
        $s21 += $carry20;
        $s20 -= $carry20 << 21;
        //    carry22 = (s22 + (int64_t) (1L << 20)) >> 21;
        //    s23 += carry22;
        //    s22 -= carry22 * ((uint64_t) 1L << 21);
        $carry22 = ($s22 + (1 << 20)) >> 21;
        $s23 += $carry22;
        $s22 -= $carry22 << 21;

        //    carry1 = (s1 + (int64_t) (1L << 20)) >> 21;
        //    s2 += carry1;
        //    s1 -= carry1 * ((uint64_t) 1L << 21);
        $carry1 = ($s1 + (1 << 20)) >> 21;
        $s2 += $carry1;
        $s1 -= $carry1 << 21;
        //    carry3 = (s3 + (int64_t) (1L << 20)) >> 21;
        //    s4 += carry3;
        //    s3 -= carry3 * ((uint64_t) 1L << 21);
        $carry3 = ($s3 + (1 << 20)) >> 21;
        $s4 += $carry3;
        $s3 -= $carry3 << 21;
        //    carry5 = (s5 + (int64_t) (1L << 20)) >> 21;
        //    s6 += carry5;
        //    s5 -= carry5 * ((uint64_t) 1L << 21);
        $carry5 = ($s5 + (1 << 20)) >> 21;
        $s6 += $carry5;
        $s5 -= $carry5 << 21;
        //    carry7 = (s7 + (int64_t) (1L << 20)) >> 21;
        //    s8 += carry7;
        //    s7 -= carry7 * ((uint64_t) 1L << 21);
        $carry7 = ($s7 + (1 << 20)) >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        //    carry9 = (s9 + (int64_t) (1L << 20)) >> 21;
        //    s10 += carry9;
        //    s9 -= carry9 * ((uint64_t) 1L << 21);
        $carry9 = ($s9 + (1 << 20)) >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        //    carry11 = (s11 + (int64_t) (1L << 20)) >> 21;
        //    s12 += carry11;
        //    s11 -= carry11 * ((uint64_t) 1L << 21);
        $carry11 = ($s11 + (1 << 20)) >> 21;
        $s12 += $carry11;
        $s11 -= $carry11 << 21;
        //    carry13 = (s13 + (int64_t) (1L << 20)) >> 21;
        //    s14 += carry13;
        //    s13 -= carry13 * ((uint64_t) 1L << 21);
        $carry13 = ($s13 + (1 << 20)) >> 21;
        $s14 += $carry13;
        $s13 -= $carry13 << 21;
        //    carry15 = (s15 + (int64_t) (1L << 20)) >> 21;
        //    s16 += carry15;
        //    s15 -= carry15 * ((uint64_t) 1L << 21);
        $carry15 = ($s15 + (1 << 20)) >> 21;
        $s16 += $carry15;
        $s15 -= $carry15 << 21;
        //    carry17 = (s17 + (int64_t) (1L << 20)) >> 21;
        //    s18 += carry17;
        //    s17 -= carry17 * ((uint64_t) 1L << 21);
        $carry17 = ($s17 + (1 << 20)) >> 21;
        $s18 += $carry17;
        $s17 -= $carry17 << 21;
        //    carry19 = (s19 + (int64_t) (1L << 20)) >> 21;
        //    s20 += carry19;
        //    s19 -= carry19 * ((uint64_t) 1L << 21);
        $carry19 = ($s19 + (1 << 20)) >> 21;
        $s20 += $carry19;
        $s19 -= $carry19 << 21;
        //    carry21 = (s21 + (int64_t) (1L << 20)) >> 21;
        //    s22 += carry21;
        //    s21 -= carry21 * ((uint64_t) 1L << 21);
        $carry21 = ($s21 + (1 << 20)) >> 21;
        $s22 += $carry21;
        $s21 -= $carry21 << 21;

        //    s11 += s23 * 666643;
        //    s12 += s23 * 470296;
        //    s13 += s23 * 654183;
        //    s14 -= s23 * 997805;
        //    s15 += s23 * 136657;
        //    s16 -= s23 * 683901;
        $s11 += self::mul($s23, 666643, 20);
        $s12 += self::mul($s23, 470296, 19);
        $s13 += self::mul($s23, 654183, 20);
        $s14 -= self::mul($s23, 997805, 20);
        $s15 += self::mul($s23, 136657, 18);
        $s16 -= self::mul($s23, 683901, 20);

        //    s10 += s22 * 666643;
        //    s11 += s22 * 470296;
        //    s12 += s22 * 654183;
        //    s13 -= s22 * 997805;
        //    s14 += s22 * 136657;
        //    s15 -= s22 * 683901;
        $s10 += self::mul($s22, 666643, 20);
        $s11 += self::mul($s22, 470296, 19);
        $s12 += self::mul($s22, 654183, 20);
        $s13 -= self::mul($s22, 997805, 20);
        $s14 += self::mul($s22, 136657, 18);
        $s15 -= self::mul($s22, 683901, 20);

        //    s9 += s21 * 666643;
        //    s10 += s21 * 470296;
        //    s11 += s21 * 654183;
        //    s12 -= s21 * 997805;
        //    s13 += s21 * 136657;
        //    s14 -= s21 * 683901;
        $s9 += self::mul($s21, 666643, 20);
        $s10 += self::mul($s21, 470296, 19);
        $s11 += self::mul($s21, 654183, 20);
        $s12 -= self::mul($s21, 997805, 20);
        $s13 += self::mul($s21, 136657, 18);
        $s14 -= self::mul($s21, 683901, 20);

        //    s8 += s20 * 666643;
        //    s9 += s20 * 470296;
        //    s10 += s20 * 654183;
        //    s11 -= s20 * 997805;
        //    s12 += s20 * 136657;
        //    s13 -= s20 * 683901;
        $s8 += self::mul($s20, 666643, 20);
        $s9 += self::mul($s20, 470296, 19);
        $s10 += self::mul($s20, 654183, 20);
        $s11 -= self::mul($s20, 997805, 20);
        $s12 += self::mul($s20, 136657, 18);
        $s13 -= self::mul($s20, 683901, 20);

        //    s7 += s19 * 666643;
        //    s8 += s19 * 470296;
        //    s9 += s19 * 654183;
        //    s10 -= s19 * 997805;
        //    s11 += s19 * 136657;
        //    s12 -= s19 * 683901;
        $s7 += self::mul($s19, 666643, 20);
        $s8 += self::mul($s19, 470296, 19);
        $s9 += self::mul($s19, 654183, 20);
        $s10 -= self::mul($s19, 997805, 20);
        $s11 += self::mul($s19, 136657, 18);
        $s12 -= self::mul($s19, 683901, 20);

        //    s6 += s18 * 666643;
        //    s7 += s18 * 470296;
        //    s8 += s18 * 654183;
        //    s9 -= s18 * 997805;
        //    s10 += s18 * 136657;
        //    s11 -= s18 * 683901;
        $s6 += self::mul($s18, 666643, 20);
        $s7 += self::mul($s18, 470296, 19);
        $s8 += self::mul($s18, 654183, 20);
        $s9 -= self::mul($s18, 997805, 20);
        $s10 += self::mul($s18, 136657, 18);
        $s11 -= self::mul($s18, 683901, 20);

        //    carry6 = (s6 + (int64_t) (1L << 20)) >> 21;
        //    s7 += carry6;
        //    s6 -= carry6 * ((uint64_t) 1L << 21);
        $carry6 = ($s6 + (1 << 20)) >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        //    carry8 = (s8 + (int64_t) (1L << 20)) >> 21;
        //    s9 += carry8;
        //    s8 -= carry8 * ((uint64_t) 1L << 21);
        $carry8 = ($s8 + (1 << 20)) >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        //    carry10 = (s10 + (int64_t) (1L << 20)) >> 21;
        //    s11 += carry10;
        //    s10 -= carry10 * ((uint64_t) 1L << 21);
        $carry10 = ($s10 + (1 << 20)) >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;
        //    carry12 = (s12 + (int64_t) (1L << 20)) >> 21;
        //    s13 += carry12;
        //    s12 -= carry12 * ((uint64_t) 1L << 21);
        $carry12 = ($s12 + (1 << 20)) >> 21;
        $s13 += $carry12;
        $s12 -= $carry12 << 21;
        //    carry14 = (s14 + (int64_t) (1L << 20)) >> 21;
        //    s15 += carry14;
        //    s14 -= carry14 * ((uint64_t) 1L << 21);
        $carry14 = ($s14 + (1 << 20)) >> 21;
        $s15 += $carry14;
        $s14 -= $carry14 << 21;
        //    carry16 = (s16 + (int64_t) (1L << 20)) >> 21;
        //    s17 += carry16;
        //    s16 -= carry16 * ((uint64_t) 1L << 21);
        $carry16 = ($s16 + (1 << 20)) >> 21;
        $s17 += $carry16;
        $s16 -= $carry16 << 21;

        //    carry7 = (s7 + (int64_t) (1L << 20)) >> 21;
        //    s8 += carry7;
        //    s7 -= carry7 * ((uint64_t) 1L << 21);
        $carry7 = ($s7 + (1 << 20)) >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        //    carry9 = (s9 + (int64_t) (1L << 20)) >> 21;
        //    s10 += carry9;
        //    s9 -= carry9 * ((uint64_t) 1L << 21);
        $carry9 = ($s9 + (1 << 20)) >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        //    carry11 = (s11 + (int64_t) (1L << 20)) >> 21;
        //    s12 += carry11;
        //    s11 -= carry11 * ((uint64_t) 1L << 21);
        $carry11 = ($s11 + (1 << 20)) >> 21;
        $s12 += $carry11;
        $s11 -= $carry11 << 21;
        //    carry13 = (s13 + (int64_t) (1L << 20)) >> 21;
        //    s14 += carry13;
        //    s13 -= carry13 * ((uint64_t) 1L << 21);
        $carry13 = ($s13 + (1 << 20)) >> 21;
        $s14 += $carry13;
        $s13 -= $carry13 << 21;
        //    carry15 = (s15 + (int64_t) (1L << 20)) >> 21;
        //    s16 += carry15;
        //    s15 -= carry15 * ((uint64_t) 1L << 21);
        $carry15 = ($s15 + (1 << 20)) >> 21;
        $s16 += $carry15;
        $s15 -= $carry15 << 21;

        //    s5 += s17 * 666643;
        //    s6 += s17 * 470296;
        //    s7 += s17 * 654183;
        //    s8 -= s17 * 997805;
        //    s9 += s17 * 136657;
        //    s10 -= s17 * 683901;
        $s5 += self::mul($s17, 666643, 20);
        $s6 += self::mul($s17, 470296, 19);
        $s7 += self::mul($s17, 654183, 20);
        $s8 -= self::mul($s17, 997805, 20);
        $s9 += self::mul($s17, 136657, 18);
        $s10 -= self::mul($s17, 683901, 20);

        //    s4 += s16 * 666643;
        //    s5 += s16 * 470296;
        //    s6 += s16 * 654183;
        //    s7 -= s16 * 997805;
        //    s8 += s16 * 136657;
        //    s9 -= s16 * 683901;
        $s4 += self::mul($s16, 666643, 20);
        $s5 += self::mul($s16, 470296, 19);
        $s6 += self::mul($s16, 654183, 20);
        $s7 -= self::mul($s16, 997805, 20);
        $s8 += self::mul($s16, 136657, 18);
        $s9 -= self::mul($s16, 683901, 20);

        //    s3 += s15 * 666643;
        //    s4 += s15 * 470296;
        //    s5 += s15 * 654183;
        //    s6 -= s15 * 997805;
        //    s7 += s15 * 136657;
        //    s8 -= s15 * 683901;
        $s3 += self::mul($s15, 666643, 20);
        $s4 += self::mul($s15, 470296, 19);
        $s5 += self::mul($s15, 654183, 20);
        $s6 -= self::mul($s15, 997805, 20);
        $s7 += self::mul($s15, 136657, 18);
        $s8 -= self::mul($s15, 683901, 20);

        //    s2 += s14 * 666643;
        //    s3 += s14 * 470296;
        //    s4 += s14 * 654183;
        //    s5 -= s14 * 997805;
        //    s6 += s14 * 136657;
        //    s7 -= s14 * 683901;
        $s2 += self::mul($s14, 666643, 20);
        $s3 += self::mul($s14, 470296, 19);
        $s4 += self::mul($s14, 654183, 20);
        $s5 -= self::mul($s14, 997805, 20);
        $s6 += self::mul($s14, 136657, 18);
        $s7 -= self::mul($s14, 683901, 20);

        //    s1 += s13 * 666643;
        //    s2 += s13 * 470296;
        //    s3 += s13 * 654183;
        //    s4 -= s13 * 997805;
        //    s5 += s13 * 136657;
        //    s6 -= s13 * 683901;
        $s1 += self::mul($s13, 666643, 20);
        $s2 += self::mul($s13, 470296, 19);
        $s3 += self::mul($s13, 654183, 20);
        $s4 -= self::mul($s13, 997805, 20);
        $s5 += self::mul($s13, 136657, 18);
        $s6 -= self::mul($s13, 683901, 20);

        //    s0 += s12 * 666643;
        //    s1 += s12 * 470296;
        //    s2 += s12 * 654183;
        //    s3 -= s12 * 997805;
        //    s4 += s12 * 136657;
        //    s5 -= s12 * 683901;
        //    s12 = 0;
        $s0 += self::mul($s12, 666643, 20);
        $s1 += self::mul($s12, 470296, 19);
        $s2 += self::mul($s12, 654183, 20);
        $s3 -= self::mul($s12, 997805, 20);
        $s4 += self::mul($s12, 136657, 18);
        $s5 -= self::mul($s12, 683901, 20);
        $s12 = 0;

        //    carry0 = (s0 + (int64_t) (1L << 20)) >> 21;
        //    s1 += carry0;
        //    s0 -= carry0 * ((uint64_t) 1L << 21);
        $carry0 = ($s0 + (1 << 20)) >> 21;
        $s1 += $carry0;
        $s0 -= $carry0 << 21;
        //    carry2 = (s2 + (int64_t) (1L << 20)) >> 21;
        //    s3 += carry2;
        //    s2 -= carry2 * ((uint64_t) 1L << 21);
        $carry2 = ($s2 + (1 << 20)) >> 21;
        $s3 += $carry2;
        $s2 -= $carry2 << 21;
        //    carry4 = (s4 + (int64_t) (1L << 20)) >> 21;
        //    s5 += carry4;
        //    s4 -= carry4 * ((uint64_t) 1L << 21);
        $carry4 = ($s4 + (1 << 20)) >> 21;
        $s5 += $carry4;
        $s4 -= $carry4 << 21;
        //    carry6 = (s6 + (int64_t) (1L << 20)) >> 21;
        //    s7 += carry6;
        //    s6 -= carry6 * ((uint64_t) 1L << 21);
        $carry6 = ($s6 + (1 << 20)) >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        //    carry8 = (s8 + (int64_t) (1L << 20)) >> 21;
        //    s9 += carry8;
        //    s8 -= carry8 * ((uint64_t) 1L << 21);
        $carry8 = ($s8 + (1 << 20)) >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        //    carry10 = (s10 + (int64_t) (1L << 20)) >> 21;
        //    s11 += carry10;
        //    s10 -= carry10 * ((uint64_t) 1L << 21);
        $carry10 = ($s10 + (1 << 20)) >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;

        //    carry1 = (s1 + (int64_t) (1L << 20)) >> 21;
        //    s2 += carry1;
        //    s1 -= carry1 * ((uint64_t) 1L << 21);
        $carry1 = ($s1 + (1 << 20)) >> 21;
        $s2 += $carry1;
        $s1 -= $carry1 << 21;
        //    carry3 = (s3 + (int64_t) (1L << 20)) >> 21;
        //    s4 += carry3;
        //    s3 -= carry3 * ((uint64_t) 1L << 21);
        $carry3 = ($s3 + (1 << 20)) >> 21;
        $s4 += $carry3;
        $s3 -= $carry3 << 21;
        //    carry5 = (s5 + (int64_t) (1L << 20)) >> 21;
        //    s6 += carry5;
        //    s5 -= carry5 * ((uint64_t) 1L << 21);
        $carry5 = ($s5 + (1 << 20)) >> 21;
        $s6 += $carry5;
        $s5 -= $carry5 << 21;
        //    carry7 = (s7 + (int64_t) (1L << 20)) >> 21;
        //    s8 += carry7;
        //    s7 -= carry7 * ((uint64_t) 1L << 21);
        $carry7 = ($s7 + (1 << 20)) >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        //    carry9 = (s9 + (int64_t) (1L << 20)) >> 21;
        //    s10 += carry9;
        //    s9 -= carry9 * ((uint64_t) 1L << 21);
        $carry9 = ($s9 + (1 << 20)) >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        //    carry11 = (s11 + (int64_t) (1L << 20)) >> 21;
        //    s12 += carry11;
        //    s11 -= carry11 * ((uint64_t) 1L << 21);
        $carry11 = ($s11 + (1 << 20)) >> 21;
        $s12 += $carry11;
        $s11 -= $carry11 << 21;

        //    s0 += s12 * 666643;
        //    s1 += s12 * 470296;
        //    s2 += s12 * 654183;
        //    s3 -= s12 * 997805;
        //    s4 += s12 * 136657;
        //    s5 -= s12 * 683901;
        //    s12 = 0;
        $s0 += self::mul($s12, 666643, 20);
        $s1 += self::mul($s12, 470296, 19);
        $s2 += self::mul($s12, 654183, 20);
        $s3 -= self::mul($s12, 997805, 20);
        $s4 += self::mul($s12, 136657, 18);
        $s5 -= self::mul($s12, 683901, 20);
        $s12 = 0;

        //    carry0 = s0 >> 21;
        //    s1 += carry0;
        //    s0 -= carry0 * ((uint64_t) 1L << 21);
        $carry0 = $s0 >> 21;
        $s1 += $carry0;
        $s0 -= $carry0 << 21;
        //    carry1 = s1 >> 21;
        //    s2 += carry1;
        //    s1 -= carry1 * ((uint64_t) 1L << 21);
        $carry1 = $s1 >> 21;
        $s2 += $carry1;
        $s1 -= $carry1 << 21;
        //    carry2 = s2 >> 21;
        //    s3 += carry2;
        //    s2 -= carry2 * ((uint64_t) 1L << 21);
        $carry2 = $s2 >> 21;
        $s3 += $carry2;
        $s2 -= $carry2 << 21;
        //    carry3 = s3 >> 21;
        //    s4 += carry3;
        //    s3 -= carry3 * ((uint64_t) 1L << 21);
        $carry3 = $s3 >> 21;
        $s4 += $carry3;
        $s3 -= $carry3 << 21;
        //    carry4 = s4 >> 21;
        //    s5 += carry4;
        //    s4 -= carry4 * ((uint64_t) 1L << 21);
        $carry4 = $s4 >> 21;
        $s5 += $carry4;
        $s4 -= $carry4 << 21;
        //    carry5 = s5 >> 21;
        //    s6 += carry5;
        //    s5 -= carry5 * ((uint64_t) 1L << 21);
        $carry5 = $s5 >> 21;
        $s6 += $carry5;
        $s5 -= $carry5 << 21;
        //    carry6 = s6 >> 21;
        //    s7 += carry6;
        //    s6 -= carry6 * ((uint64_t) 1L << 21);
        $carry6 = $s6 >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        //    carry7 = s7 >> 21;
        //    s8 += carry7;
        //    s7 -= carry7 * ((uint64_t) 1L << 21);
        $carry7 = $s7 >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        //    carry8 = s8 >> 21;
        //    s9 += carry8;
        //    s8 -= carry8 * ((uint64_t) 1L << 21);
        $carry8 = $s8 >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        //    carry9 = s9 >> 21;
        //    s10 += carry9;
        //    s9 -= carry9 * ((uint64_t) 1L << 21);
        $carry9 = $s9 >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        //    carry10 = s10 >> 21;
        //    s11 += carry10;
        //    s10 -= carry10 * ((uint64_t) 1L << 21);
        $carry10 = $s10 >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;
        //    carry11 = s11 >> 21;
        //    s12 += carry11;
        //    s11 -= carry11 * ((uint64_t) 1L << 21);
        $carry11 = $s11 >> 21;
        $s12 += $carry11;
        $s11 -= $carry11 << 21;

        //    s0 += s12 * 666643;
        //    s1 += s12 * 470296;
        //    s2 += s12 * 654183;
        //    s3 -= s12 * 997805;
        //    s4 += s12 * 136657;
        //    s5 -= s12 * 683901;
        $s0 += self::mul($s12, 666643, 20);
        $s1 += self::mul($s12, 470296, 19);
        $s2 += self::mul($s12, 654183, 20);
        $s3 -= self::mul($s12, 997805, 20);
        $s4 += self::mul($s12, 136657, 18);
        $s5 -= self::mul($s12, 683901, 20);

        //    carry0 = s0 >> 21;
        //    s1 += carry0;
        //    s0 -= carry0 * ((uint64_t) 1L << 21);
        $carry0 = $s0 >> 21;
        $s1 += $carry0;
        $s0 -= $carry0 << 21;
        //    carry1 = s1 >> 21;
        //    s2 += carry1;
        //    s1 -= carry1 * ((uint64_t) 1L << 21);
        $carry1 = $s1 >> 21;
        $s2 += $carry1;
        $s1 -= $carry1 << 21;
        //    carry2 = s2 >> 21;
        //    s3 += carry2;
        //    s2 -= carry2 * ((uint64_t) 1L << 21);
        $carry2 = $s2 >> 21;
        $s3 += $carry2;
        $s2 -= $carry2 << 21;
        //    carry3 = s3 >> 21;
        //    s4 += carry3;
        //    s3 -= carry3 * ((uint64_t) 1L << 21);
        $carry3 = $s3 >> 21;
        $s4 += $carry3;
        $s3 -= $carry3 << 21;
        //    carry4 = s4 >> 21;
        //    s5 += carry4;
        //    s4 -= carry4 * ((uint64_t) 1L << 21);
        $carry4 = $s4 >> 21;
        $s5 += $carry4;
        $s4 -= $carry4 << 21;
        //    carry5 = s5 >> 21;
        //    s6 += carry5;
        //    s5 -= carry5 * ((uint64_t) 1L << 21);
        $carry5 = $s5 >> 21;
        $s6 += $carry5;
        $s5 -= $carry5 << 21;
        //    carry6 = s6 >> 21;
        //    s7 += carry6;
        //    s6 -= carry6 * ((uint64_t) 1L << 21);
        $carry6 = $s6 >> 21;
        $s7 += $carry6;
        $s6 -= $carry6 << 21;
        //    carry7 = s7 >> 21;
        //    s8 += carry7;
        //    s7 -= carry7 * ((uint64_t) 1L << 21);
        $carry7 = $s7 >> 21;
        $s8 += $carry7;
        $s7 -= $carry7 << 21;
        //    carry8 = s8 >> 21;
        //    s9 += carry8;
        //    s8 -= carry8 * ((uint64_t) 1L << 21);
        $carry8 = $s8 >> 21;
        $s9 += $carry8;
        $s8 -= $carry8 << 21;
        //    carry9 = s9 >> 21;
        //    s10 += carry9;
        //    s9 -= carry9 * ((uint64_t) 1L << 21);
        $carry9 = $s9 >> 21;
        $s10 += $carry9;
        $s9 -= $carry9 << 21;
        //    carry10 = s10 >> 21;
        //    s11 += carry10;
        //    s10 -= carry10 * ((uint64_t) 1L << 21);
        $carry10 = $s10 >> 21;
        $s11 += $carry10;
        $s10 -= $carry10 << 21;

        $s = array_fill(0, 32, 0);
        // s[0]  = s0 >> 0;
        $s[0]  = $s0 >> 0;
        // s[1]  = s0 >> 8;
        $s[1]  = $s0 >> 8;
        // s[2]  = (s0 >> 16) | (s1 * ((uint64_t) 1 << 5));
        $s[2]  = ($s0 >> 16) | ($s1 << 5);
        // s[3]  = s1 >> 3;
        $s[3]  = $s1 >> 3;
        // s[4]  = s1 >> 11;
        $s[4]  = $s1 >> 11;
        // s[5]  = (s1 >> 19) | (s2 * ((uint64_t) 1 << 2));
        $s[5]  = ($s1 >> 19) | ($s2 << 2);
        // s[6]  = s2 >> 6;
        $s[6]  = $s2 >> 6;
        // s[7]  = (s2 >> 14) | (s3 * ((uint64_t) 1 << 7));
        $s[7]  = ($s2 >> 14) | ($s3 << 7);
        // s[8]  = s3 >> 1;
        $s[8]  = $s3 >> 1;
        // s[9]  = s3 >> 9;
        $s[9]  = $s3 >> 9;
        // s[10] = (s3 >> 17) | (s4 * ((uint64_t) 1 << 4));
        $s[10] = ($s3 >> 17) | ($s4 << 4);
        // s[11] = s4 >> 4;
        $s[11] = $s4 >> 4;
        // s[12] = s4 >> 12;
        $s[12] = $s4 >> 12;
        // s[13] = (s4 >> 20) | (s5 * ((uint64_t) 1 << 1));
        $s[13] = ($s4 >> 20) | ($s5 << 1);
        // s[14] = s5 >> 7;
        $s[14] = $s5 >> 7;
        // s[15] = (s5 >> 15) | (s6 * ((uint64_t) 1 << 6));
        $s[15] = ($s5 >> 15) | ($s6 << 6);
        // s[16] = s6 >> 2;
        $s[16] = $s6 >> 2;
        // s[17] = s6 >> 10;
        $s[17] = $s6 >> 10;
        // s[18] = (s6 >> 18) | (s7 * ((uint64_t) 1 << 3));
        $s[18] = ($s6 >> 18) | ($s7 << 3);
        // s[19] = s7 >> 5;
        $s[19] = $s7 >> 5;
        // s[20] = s7 >> 13;
        $s[20] = $s7 >> 13;
        // s[21] = s8 >> 0;
        $s[21] = $s8 >> 0;
        // s[22] = s8 >> 8;
        $s[22] = $s8 >> 8;
        // s[23] = (s8 >> 16) | (s9 * ((uint64_t) 1 << 5));
        $s[23] = ($s8 >> 16) | ($s9 << 5);
        // s[24] = s9 >> 3;
        $s[24] = $s9 >> 3;
        // s[25] = s9 >> 11;
        $s[25] = $s9 >> 11;
        // s[26] = (s9 >> 19) | (s10 * ((uint64_t) 1 << 2));
        $s[26] = ($s9 >> 19) | ($s10 << 2);
        // s[27] = s10 >> 6;
        $s[27] = $s10 >> 6;
        // s[28] = (s10 >> 14) | (s11 * ((uint64_t) 1 << 7));
        $s[28] = ($s10 >> 14) | ($s11 << 7);
        // s[29] = s11 >> 1;
        $s[29] = $s11 >> 1;
        // s[30] = s11 >> 9;
        $s[30] = $s11 >> 9;
        // s[31] = s11 >> 17;
        $s[31] = $s11 >> 17;
        return self::intArrayToString($s);
    }

    /**
     * @param string $s
     * @return string
     */
    public static function sc25519_sq($s)
    {
        return self::sc25519_mul($s, $s);
    }

    /**
     * @param string $s
     * @param int $n
     * @param string $a
     * @return string
     */
    public static function sc25519_sqmul($s, $n, $a)
    {
        for ($i = 0; $i < $n; ++$i) {
            $s = self::sc25519_sq($s);
        }
        return self::sc25519_mul($s, $a);
    }

    /**
     * @param string $s
     * @return string
     */
    public static function sc25519_invert($s)
    {
        $_10 = self::sc25519_sq($s);
        $_11 = self::sc25519_mul($s, $_10);
        $_100 = self::sc25519_mul($s, $_11);
        $_1000 = self::sc25519_sq($_100);
        $_1010 = self::sc25519_mul($_10, $_1000);
        $_1011 = self::sc25519_mul($s, $_1010);
        $_10000 = self::sc25519_sq($_1000);
        $_10110 = self::sc25519_sq($_1011);
        $_100000 = self::sc25519_mul($_1010, $_10110);
        $_100110 = self::sc25519_mul($_10000, $_10110);
        $_1000000 = self::sc25519_sq($_100000);
        $_1010000 = self::sc25519_mul($_10000, $_1000000);
        $_1010011 = self::sc25519_mul($_11, $_1010000);
        $_1100011 = self::sc25519_mul($_10000, $_1010011);
        $_1100111 = self::sc25519_mul($_100, $_1100011);
        $_1101011 = self::sc25519_mul($_100, $_1100111);
        $_10010011 = self::sc25519_mul($_1000000, $_1010011);
        $_10010111 = self::sc25519_mul($_100, $_10010011);
        $_10111101 = self::sc25519_mul($_100110, $_10010111);
        $_11010011 = self::sc25519_mul($_10110, $_10111101);
        $_11100111 = self::sc25519_mul($_1010000, $_10010111);
        $_11101011 = self::sc25519_mul($_100, $_11100111);
        $_11110101 = self::sc25519_mul($_1010, $_11101011);

        $recip = self::sc25519_mul($_1011, $_11110101);
        $recip = self::sc25519_sqmul($recip, 126, $_1010011);
        $recip = self::sc25519_sqmul($recip, 9, $_10);
        $recip = self::sc25519_mul($recip, $_11110101);
        $recip = self::sc25519_sqmul($recip, 7, $_1100111);
        $recip = self::sc25519_sqmul($recip, 9, $_11110101);
        $recip = self::sc25519_sqmul($recip, 11, $_10111101);
        $recip = self::sc25519_sqmul($recip, 8, $_11100111);
        $recip = self::sc25519_sqmul($recip, 9, $_1101011);
        $recip = self::sc25519_sqmul($recip, 6, $_1011);
        $recip = self::sc25519_sqmul($recip, 14, $_10010011);
        $recip = self::sc25519_sqmul($recip, 10, $_1100011);
        $recip = self::sc25519_sqmul($recip, 9, $_10010111);
        $recip = self::sc25519_sqmul($recip, 10, $_11110101);
        $recip = self::sc25519_sqmul($recip, 8, $_11010011);
        return self::sc25519_sqmul($recip, 8, $_11101011);
    }

    /**
     * @param string $s
     * @return string
     */
    public static function clamp($s)
    {
        $s_ = self::stringToIntArray($s);
        $s_[0] &= 248;
        $s_[31] |= 64;
        $s_[31] &= 128;
        return self::intArrayToString($s_);
    }
}
Core/SecretStream/State.php000064400000007050152331075510011632 0ustar00<?php

/**
 * Class ParagonIE_Sodium_Core_SecretStream_State
 */
class ParagonIE_Sodium_Core_SecretStream_State
{
    /** @var string $key */
    protected $key;

    /** @var int $counter */
    protected $counter;

    /** @var string $nonce */
    protected $nonce;

    /** @var string $_pad */
    protected $_pad;

    /**
     * ParagonIE_Sodium_Core_SecretStream_State constructor.
     * @param string $key
     * @param string|null $nonce
     */
    public function __construct($key, $nonce = null)
    {
        $this->key = $key;
        $this->counter = 1;
        if (is_null($nonce)) {
            $nonce = str_repeat("\0", 12);
        }
        $this->nonce = str_pad($nonce, 12, "\0", STR_PAD_RIGHT);;
        $this->_pad = str_repeat("\0", 4);
    }

    /**
     * @return self
     */
    public function counterReset()
    {
        $this->counter = 1;
        $this->_pad = str_repeat("\0", 4);
        return $this;
    }

    /**
     * @return string
     */
    public function getKey()
    {
        return $this->key;
    }

    /**
     * @return string
     */
    public function getCounter()
    {
        return ParagonIE_Sodium_Core_Util::store32_le($this->counter);
    }

    /**
     * @return string
     */
    public function getNonce()
    {
        if (!is_string($this->nonce)) {
            $this->nonce = str_repeat("\0", 12);
        }
        if (ParagonIE_Sodium_Core_Util::strlen($this->nonce) !== 12) {
            $this->nonce = str_pad($this->nonce, 12, "\0", STR_PAD_RIGHT);
        }
        return $this->nonce;
    }

    /**
     * @return string
     */
    public function getCombinedNonce()
    {
        return $this->getCounter() .
            ParagonIE_Sodium_Core_Util::substr($this->getNonce(), 0, 8);
    }

    /**
     * @return self
     */
    public function incrementCounter()
    {
        ++$this->counter;
        return $this;
    }

    /**
     * @return bool
     */
    public function needsRekey()
    {
        return ($this->counter & 0xffff) === 0;
    }

    /**
     * @param string $newKeyAndNonce
     * @return self
     */
    public function rekey($newKeyAndNonce)
    {
        $this->key = ParagonIE_Sodium_Core_Util::substr($newKeyAndNonce, 0, 32);
        $this->nonce = str_pad(
            ParagonIE_Sodium_Core_Util::substr($newKeyAndNonce, 32),
            12,
            "\0",
            STR_PAD_RIGHT
        );
        return $this;
    }

    /**
     * @param string $str
     * @return self
     */
    public function xorNonce($str)
    {
        $this->nonce = ParagonIE_Sodium_Core_Util::xorStrings(
            $this->getNonce(),
            str_pad(
                ParagonIE_Sodium_Core_Util::substr($str, 0, 8),
                12,
                "\0",
                STR_PAD_RIGHT
            )
        );
        return $this;
    }

    /**
     * @param string $string
     * @return self
     */
    public static function fromString($string)
    {
        $state = new ParagonIE_Sodium_Core_SecretStream_State(
            ParagonIE_Sodium_Core_Util::substr($string, 0, 32)
        );
        $state->counter = ParagonIE_Sodium_Core_Util::load_4(
            ParagonIE_Sodium_Core_Util::substr($string, 32, 4)
        );
        $state->nonce = ParagonIE_Sodium_Core_Util::substr($string, 36, 12);
        $state->_pad = ParagonIE_Sodium_Core_Util::substr($string, 48, 8);
        return $state;
    }

    /**
     * @return string
     */
    public function toString()
    {
        return $this->key .
            $this->getCounter() .
            $this->nonce .
            $this->_pad;
    }
}
Core/Util.php000064400000066645152331075510007105 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_Util', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_Util
 */
abstract class ParagonIE_Sodium_Core_Util
{
    /**
     * @param int $integer
     * @param int $size (16, 32, 64)
     * @return int
     */
    public static function abs($integer, $size = 0)
    {
        /** @var int $realSize */
        $realSize = (PHP_INT_SIZE << 3) - 1;
        if ($size) {
            --$size;
        } else {
            /** @var int $size */
            $size = $realSize;
        }

        $negative = -(($integer >> $size) & 1);
        return (int) (
            ($integer ^ $negative)
                +
            (($negative >> $realSize) & 1)
        );
    }

    /**
     * Convert a binary string into a hexadecimal string without cache-timing
     * leaks
     *
     * @internal You should not use this directly from another application
     *
     * @param string $binaryString (raw binary)
     * @return string
     * @throws TypeError
     */
    public static function bin2hex($binaryString)
    {
        /* Type checks: */
        if (!is_string($binaryString)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($binaryString) . ' given.');
        }

        $hex = '';
        $len = self::strlen($binaryString);
        for ($i = 0; $i < $len; ++$i) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C', $binaryString[$i]);
            /** @var int $c */
            $c = $chunk[1] & 0xf;
            /** @var int $b */
            $b = $chunk[1] >> 4;
            $hex .= pack(
                'CC',
                (87 + $b + ((($b - 10) >> 8) & ~38)),
                (87 + $c + ((($c - 10) >> 8) & ~38))
            );
        }
        return $hex;
    }

    /**
     * Convert a binary string into a hexadecimal string without cache-timing
     * leaks, returning uppercase letters (as per RFC 4648)
     *
     * @internal You should not use this directly from another application
     *
     * @param string $bin_string (raw binary)
     * @return string
     * @throws TypeError
     */
    public static function bin2hexUpper($bin_string)
    {
        $hex = '';
        $len = self::strlen($bin_string);
        for ($i = 0; $i < $len; ++$i) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C', $bin_string[$i]);
            /**
             * Lower 16 bits
             *
             * @var int $c
             */
            $c = $chunk[1] & 0xf;

            /**
             * Upper 16 bits
             * @var int $b
             */
            $b = $chunk[1] >> 4;

            /**
             * Use pack() and binary operators to turn the two integers
             * into hexadecimal characters. We don't use chr() here, because
             * it uses a lookup table internally and we want to avoid
             * cache-timing side-channels.
             */
            $hex .= pack(
                'CC',
                (55 + $b + ((($b - 10) >> 8) & ~6)),
                (55 + $c + ((($c - 10) >> 8) & ~6))
            );
        }
        return $hex;
    }

    /**
     * Cache-timing-safe variant of ord()
     *
     * @internal You should not use this directly from another application
     *
     * @param string $chr
     * @return int
     * @throws SodiumException
     * @throws TypeError
     */
    public static function chrToInt($chr)
    {
        /* Type checks: */
        if (!is_string($chr)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($chr) . ' given.');
        }
        if (self::strlen($chr) !== 1) {
            throw new SodiumException('chrToInt() expects a string that is exactly 1 character long');
        }
        /** @var array<int, int> $chunk */
        $chunk = unpack('C', $chr);
        return (int) ($chunk[1]);
    }

    /**
     * Compares two strings.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $left
     * @param string $right
     * @param int $len
     * @return int
     * @throws SodiumException
     * @throws TypeError
     */
    public static function compare($left, $right, $len = null)
    {
        $leftLen = self::strlen($left);
        $rightLen = self::strlen($right);
        if ($len === null) {
            $len = max($leftLen, $rightLen);
            $left = str_pad($left, $len, "\x00", STR_PAD_RIGHT);
            $right = str_pad($right, $len, "\x00", STR_PAD_RIGHT);
        }

        $gt = 0;
        $eq = 1;
        $i = $len;
        while ($i !== 0) {
            --$i;
            $gt |= ((self::chrToInt($right[$i]) - self::chrToInt($left[$i])) >> 8) & $eq;
            $eq &= ((self::chrToInt($right[$i]) ^ self::chrToInt($left[$i])) - 1) >> 8;
        }
        return ($gt + $gt + $eq) - 1;
    }

    /**
     * If a variable does not match a given type, throw a TypeError.
     *
     * @param mixed $mixedVar
     * @param string $type
     * @param int $argumentIndex
     * @throws TypeError
     * @throws SodiumException
     * @return void
     */
    public static function declareScalarType(&$mixedVar = null, $type = 'void', $argumentIndex = 0)
    {
        if (func_num_args() === 0) {
            /* Tautology, by default */
            return;
        }
        if (func_num_args() === 1) {
            throw new TypeError('Declared void, but passed a variable');
        }
        $realType = strtolower(gettype($mixedVar));
        $type = strtolower($type);
        switch ($type) {
            case 'null':
                if ($mixedVar !== null) {
                    throw new TypeError('Argument ' . $argumentIndex . ' must be null, ' . $realType . ' given.');
                }
                break;
            case 'integer':
            case 'int':
                $allow = array('int', 'integer');
                if (!in_array($type, $allow)) {
                    throw new TypeError('Argument ' . $argumentIndex . ' must be an integer, ' . $realType . ' given.');
                }
                $mixedVar = (int) $mixedVar;
                break;
            case 'boolean':
            case 'bool':
                $allow = array('bool', 'boolean');
                if (!in_array($type, $allow)) {
                    throw new TypeError('Argument ' . $argumentIndex . ' must be a boolean, ' . $realType . ' given.');
                }
                $mixedVar = (bool) $mixedVar;
                break;
            case 'string':
                if (!is_string($mixedVar)) {
                    throw new TypeError('Argument ' . $argumentIndex . ' must be a string, ' . $realType . ' given.');
                }
                $mixedVar = (string) $mixedVar;
                break;
            case 'decimal':
            case 'double':
            case 'float':
                $allow = array('decimal', 'double', 'float');
                if (!in_array($type, $allow)) {
                    throw new TypeError('Argument ' . $argumentIndex . ' must be a float, ' . $realType . ' given.');
                }
                $mixedVar = (float) $mixedVar;
                break;
            case 'object':
                if (!is_object($mixedVar)) {
                    throw new TypeError('Argument ' . $argumentIndex . ' must be an object, ' . $realType . ' given.');
                }
                break;
            case 'array':
                if (!is_array($mixedVar)) {
                    if (is_object($mixedVar)) {
                        if ($mixedVar instanceof ArrayAccess) {
                            return;
                        }
                    }
                    throw new TypeError('Argument ' . $argumentIndex . ' must be an array, ' . $realType . ' given.');
                }
                break;
            default:
                throw new SodiumException('Unknown type (' . $realType .') does not match expect type (' . $type . ')');
        }
    }

    /**
     * Evaluate whether or not two strings are equal (in constant-time)
     *
     * @param string $left
     * @param string $right
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function hashEquals($left, $right)
    {
        /* Type checks: */
        if (!is_string($left)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($left) . ' given.');
        }
        if (!is_string($right)) {
            throw new TypeError('Argument 2 must be a string, ' . gettype($right) . ' given.');
        }

        if (is_callable('hash_equals')) {
            return hash_equals($left, $right);
        }
        $d = 0;
        /** @var int $len */
        $len = self::strlen($left);
        if ($len !== self::strlen($right)) {
            return false;
        }
        for ($i = 0; $i < $len; ++$i) {
            $d |= self::chrToInt($left[$i]) ^ self::chrToInt($right[$i]);
        }

        if ($d !== 0) {
            return false;
        }
        return $left === $right;
    }

    /**
     * Catch hash_update() failures and throw instead of silently proceding
     *
     * @param HashContext|resource &$hs
     * @param string $data
     * @return void
     * @throws SodiumException
     * @psalm-suppress PossiblyInvalidArgument
     */
    protected static function hash_update(&$hs, $data)
    {
        if (!hash_update($hs, $data)) {
            throw new SodiumException('hash_update() failed');
        }
    }

    /**
     * Convert a hexadecimal string into a binary string without cache-timing
     * leaks
     *
     * @internal You should not use this directly from another application
     *
     * @param string $hexString
     * @param bool $strictPadding
     * @return string (raw binary)
     * @throws RangeException
     * @throws TypeError
     */
    public static function hex2bin($hexString, $strictPadding = false)
    {
        /* Type checks: */
        if (!is_string($hexString)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($hexString) . ' given.');
        }

        /** @var int $hex_pos */
        $hex_pos = 0;
        /** @var string $bin */
        $bin = '';
        /** @var int $c_acc */
        $c_acc = 0;
        /** @var int $hex_len */
        $hex_len = self::strlen($hexString);
        /** @var int $state */
        $state = 0;
        if (($hex_len & 1) !== 0) {
            if ($strictPadding) {
                throw new RangeException(
                    'Expected an even number of hexadecimal characters'
                );
            } else {
                $hexString = '0' . $hexString;
                ++$hex_len;
            }
        }

        $chunk = unpack('C*', $hexString);
        while ($hex_pos < $hex_len) {
            ++$hex_pos;
            /** @var int $c */
            $c = $chunk[$hex_pos];
            /** @var int $c_num */
            $c_num = $c ^ 48;
            /** @var int $c_num0 */
            $c_num0 = ($c_num - 10) >> 8;
            /** @var int $c_alpha */
            $c_alpha = ($c & ~32) - 55;
            /** @var int $c_alpha0 */
            $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8;
            if (($c_num0 | $c_alpha0) === 0) {
                throw new RangeException(
                    'hex2bin() only expects hexadecimal characters'
                );
            }
            /** @var int $c_val */
            $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0);
            if ($state === 0) {
                $c_acc = $c_val * 16;
            } else {
                $bin .= pack('C', $c_acc | $c_val);
            }
            $state ^= 1;
        }
        return $bin;
    }

    /**
     * Turn an array of integers into a string
     *
     * @internal You should not use this directly from another application
     *
     * @param array<int, int> $ints
     * @return string
     */
    public static function intArrayToString(array $ints)
    {
        /** @var array<int, int> $args */
        $args = $ints;
        foreach ($args as $i => $v) {
            $args[$i] = (int) ($v & 0xff);
        }
        array_unshift($args, str_repeat('C', count($ints)));
        return (string) (call_user_func_array('pack', $args));
    }

    /**
     * Cache-timing-safe variant of ord()
     *
     * @internal You should not use this directly from another application
     *
     * @param int $int
     * @return string
     * @throws TypeError
     */
    public static function intToChr($int)
    {
        return pack('C', $int);
    }

    /**
     * Load a 3 character substring into an integer
     *
     * @internal You should not use this directly from another application
     *
     * @param string $string
     * @return int
     * @throws RangeException
     * @throws TypeError
     */
    public static function load_3($string)
    {
        /* Type checks: */
        if (!is_string($string)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($string) . ' given.');
        }

        /* Input validation: */
        if (self::strlen($string) < 3) {
            throw new RangeException(
                'String must be 3 bytes or more; ' . self::strlen($string) . ' given.'
            );
        }
        /** @var array<int, int> $unpacked */
        $unpacked = unpack('V', $string . "\0");
        return (int) ($unpacked[1] & 0xffffff);
    }

    /**
     * Load a 4 character substring into an integer
     *
     * @internal You should not use this directly from another application
     *
     * @param string $string
     * @return int
     * @throws RangeException
     * @throws TypeError
     */
    public static function load_4($string)
    {
        /* Type checks: */
        if (!is_string($string)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($string) . ' given.');
        }

        /* Input validation: */
        if (self::strlen($string) < 4) {
            throw new RangeException(
                'String must be 4 bytes or more; ' . self::strlen($string) . ' given.'
            );
        }
        /** @var array<int, int> $unpacked */
        $unpacked = unpack('V', $string);
        return (int) ($unpacked[1] & 0xffffffff);
    }

    /**
     * Load a 8 character substring into an integer
     *
     * @internal You should not use this directly from another application
     *
     * @param string $string
     * @return int
     * @throws RangeException
     * @throws SodiumException
     * @throws TypeError
     */
    public static function load64_le($string)
    {
        /* Type checks: */
        if (!is_string($string)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($string) . ' given.');
        }

        /* Input validation: */
        if (self::strlen($string) < 4) {
            throw new RangeException(
                'String must be 4 bytes or more; ' . self::strlen($string) . ' given.'
            );
        }
        if (PHP_VERSION_ID >= 50603 && PHP_INT_SIZE === 8) {
            /** @var array<int, int> $unpacked */
            $unpacked = unpack('P', $string);
            return (int) $unpacked[1];
        }

        /** @var int $result */
        $result  = (self::chrToInt($string[0]) & 0xff);
        $result |= (self::chrToInt($string[1]) & 0xff) <<  8;
        $result |= (self::chrToInt($string[2]) & 0xff) << 16;
        $result |= (self::chrToInt($string[3]) & 0xff) << 24;
        $result |= (self::chrToInt($string[4]) & 0xff) << 32;
        $result |= (self::chrToInt($string[5]) & 0xff) << 40;
        $result |= (self::chrToInt($string[6]) & 0xff) << 48;
        $result |= (self::chrToInt($string[7]) & 0xff) << 56;
        return (int) $result;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $left
     * @param string $right
     * @return int
     * @throws SodiumException
     * @throws TypeError
     */
    public static function memcmp($left, $right)
    {
        if (self::hashEquals($left, $right)) {
            return 0;
        }
        return -1;
    }

    /**
     * Multiply two integers in constant-time
     *
     * Micro-architecture timing side-channels caused by how your CPU
     * implements multiplication are best prevented by never using the
     * multiplication operators and ensuring that our code always takes
     * the same number of operations to complete, regardless of the values
     * of $a and $b.
     *
     * @internal You should not use this directly from another application
     *
     * @param int $a
     * @param int $b
     * @param int $size Limits the number of operations (useful for small,
     *                  constant operands)
     * @return int
     */
    public static function mul($a, $b, $size = 0)
    {
        if (ParagonIE_Sodium_Compat::$fastMult) {
            return (int) ($a * $b);
        }

        static $defaultSize = null;
        /** @var int $defaultSize */
        if (!$defaultSize) {
            /** @var int $defaultSize */
            $defaultSize = (PHP_INT_SIZE << 3) - 1;
        }
        if ($size < 1) {
            /** @var int $size */
            $size = $defaultSize;
        }
        /** @var int $size */

        $c = 0;

        /**
         * Mask is either -1 or 0.
         *
         * -1 in binary looks like 0x1111 ... 1111
         *  0 in binary looks like 0x0000 ... 0000
         *
         * @var int
         */
        $mask = -(($b >> ((int) $defaultSize)) & 1);

        /**
         * Ensure $b is a positive integer, without creating
         * a branching side-channel
         *
         * @var int $b
         */
        $b = ($b & ~$mask) | ($mask & -$b);

        /**
         * Unless $size is provided:
         *
         * This loop always runs 32 times when PHP_INT_SIZE is 4.
         * This loop always runs 64 times when PHP_INT_SIZE is 8.
         */
        for ($i = $size; $i >= 0; --$i) {
            $c += (int) ($a & -($b & 1));
            $a <<= 1;
            $b >>= 1;
        }

        /**
         * If $b was negative, we then apply the same value to $c here.
         * It doesn't matter much if $a was negative; the $c += above would
         * have produced a negative integer to begin with. But a negative $b
         * makes $b >>= 1 never return 0, so we would end up with incorrect
         * results.
         *
         * The end result is what we'd expect from integer multiplication.
         */
        return (int) (($c & ~$mask) | ($mask & -$c));
    }

    /**
     * Convert any arbitrary numbers into two 32-bit integers that represent
     * a 64-bit integer.
     *
     * @internal You should not use this directly from another application
     *
     * @param int|float $num
     * @return array<int, int>
     */
    public static function numericTo64BitInteger($num)
    {
        $high = 0;
        /** @var int $low */
        $low = $num & 0xffffffff;

        if ((+(abs($num))) >= 1) {
            if ($num > 0) {
                /** @var int $high */
                $high = min((+(floor($num/4294967296))), 4294967295);
            } else {
                /** @var int $high */
                $high = ~~((+(ceil(($num - (+((~~($num)))))/4294967296))));
            }
        }
        return array((int) $high, (int) $low);
    }

    /**
     * Store a 24-bit integer into a string, treating it as big-endian.
     *
     * @internal You should not use this directly from another application
     *
     * @param int $int
     * @return string
     * @throws TypeError
     */
    public static function store_3($int)
    {
        /* Type checks: */
        if (!is_int($int)) {
            if (is_numeric($int)) {
                $int = (int) $int;
            } else {
                throw new TypeError('Argument 1 must be an integer, ' . gettype($int) . ' given.');
            }
        }
        /** @var string $packed */
        $packed = pack('N', $int);
        return self::substr($packed, 1, 3);
    }

    /**
     * Store a 32-bit integer into a string, treating it as little-endian.
     *
     * @internal You should not use this directly from another application
     *
     * @param int $int
     * @return string
     * @throws TypeError
     */
    public static function store32_le($int)
    {
        /* Type checks: */
        if (!is_int($int)) {
            if (is_numeric($int)) {
                $int = (int) $int;
            } else {
                throw new TypeError('Argument 1 must be an integer, ' . gettype($int) . ' given.');
            }
        }

        /** @var string $packed */
        $packed = pack('V', $int);
        return $packed;
    }

    /**
     * Store a 32-bit integer into a string, treating it as big-endian.
     *
     * @internal You should not use this directly from another application
     *
     * @param int $int
     * @return string
     * @throws TypeError
     */
    public static function store_4($int)
    {
        /* Type checks: */
        if (!is_int($int)) {
            if (is_numeric($int)) {
                $int = (int) $int;
            } else {
                throw new TypeError('Argument 1 must be an integer, ' . gettype($int) . ' given.');
            }
        }

        /** @var string $packed */
        $packed = pack('N', $int);
        return $packed;
    }

    /**
     * Stores a 64-bit integer as an string, treating it as little-endian.
     *
     * @internal You should not use this directly from another application
     *
     * @param int $int
     * @return string
     * @throws TypeError
     */
    public static function store64_le($int)
    {
        /* Type checks: */
        if (!is_int($int)) {
            if (is_numeric($int)) {
                $int = (int) $int;
            } else {
                throw new TypeError('Argument 1 must be an integer, ' . gettype($int) . ' given.');
            }
        }

        if (PHP_INT_SIZE === 8) {
            if (PHP_VERSION_ID >= 50603) {
                /** @var string $packed */
                $packed = pack('P', $int);
                return $packed;
            }
            return self::intToChr($int & 0xff) .
                self::intToChr(($int >>  8) & 0xff) .
                self::intToChr(($int >> 16) & 0xff) .
                self::intToChr(($int >> 24) & 0xff) .
                self::intToChr(($int >> 32) & 0xff) .
                self::intToChr(($int >> 40) & 0xff) .
                self::intToChr(($int >> 48) & 0xff) .
                self::intToChr(($int >> 56) & 0xff);
        }
        if ($int > PHP_INT_MAX) {
            list($hiB, $int) = self::numericTo64BitInteger($int);
        } else {
            $hiB = 0;
        }
        return
            self::intToChr(($int      ) & 0xff) .
            self::intToChr(($int >>  8) & 0xff) .
            self::intToChr(($int >> 16) & 0xff) .
            self::intToChr(($int >> 24) & 0xff) .
            self::intToChr($hiB & 0xff) .
            self::intToChr(($hiB >>  8) & 0xff) .
            self::intToChr(($hiB >> 16) & 0xff) .
            self::intToChr(($hiB >> 24) & 0xff);
    }

    /**
     * Safe string length
     *
     * @internal You should not use this directly from another application
     *
     * @ref mbstring.func_overload
     *
     * @param string $str
     * @return int
     * @throws TypeError
     */
    public static function strlen($str)
    {
        /* Type checks: */
        if (!is_string($str)) {
            throw new TypeError('String expected');
        }

        return (int) (
        self::isMbStringOverride()
            ? mb_strlen($str, '8bit')
            : strlen($str)
        );
    }

    /**
     * Turn a string into an array of integers
     *
     * @internal You should not use this directly from another application
     *
     * @param string $string
     * @return array<int, int>
     * @throws TypeError
     */
    public static function stringToIntArray($string)
    {
        if (!is_string($string)) {
            throw new TypeError('String expected');
        }
        /**
         * @var array<int, int>
         */
        $values = array_values(
            unpack('C*', $string)
        );
        return $values;
    }

    /**
     * Safe substring
     *
     * @internal You should not use this directly from another application
     *
     * @ref mbstring.func_overload
     *
     * @param string $str
     * @param int $start
     * @param int $length
     * @return string
     * @throws TypeError
     */
    public static function substr($str, $start = 0, $length = null)
    {
        /* Type checks: */
        if (!is_string($str)) {
            throw new TypeError('String expected');
        }

        if ($length === 0) {
            return '';
        }

        if (self::isMbStringOverride()) {
            if (PHP_VERSION_ID < 50400 && $length === null) {
                $length = self::strlen($str);
            }
            $sub = (string) mb_substr($str, $start, $length, '8bit');
        } elseif ($length === null) {
            $sub = (string) substr($str, $start);
        } else {
            $sub = (string) substr($str, $start, $length);
        }
        if ($sub !== '') {
            return $sub;
        }
        return '';
    }

    /**
     * Compare a 16-character byte string in constant time.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $a
     * @param string $b
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function verify_16($a, $b)
    {
        /* Type checks: */
        if (!is_string($a)) {
            throw new TypeError('String expected');
        }
        if (!is_string($b)) {
            throw new TypeError('String expected');
        }
        return self::hashEquals(
            self::substr($a, 0, 16),
            self::substr($b, 0, 16)
        );
    }

    /**
     * Compare a 32-character byte string in constant time.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $a
     * @param string $b
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function verify_32($a, $b)
    {
        /* Type checks: */
        if (!is_string($a)) {
            throw new TypeError('String expected');
        }
        if (!is_string($b)) {
            throw new TypeError('String expected');
        }
        return self::hashEquals(
            self::substr($a, 0, 32),
            self::substr($b, 0, 32)
        );
    }

    /**
     * Calculate $a ^ $b for two strings.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $a
     * @param string $b
     * @return string
     * @throws TypeError
     */
    public static function xorStrings($a, $b)
    {
        /* Type checks: */
        if (!is_string($a)) {
            throw new TypeError('Argument 1 must be a string');
        }
        if (!is_string($b)) {
            throw new TypeError('Argument 2 must be a string');
        }

        return (string) ($a ^ $b);
    }

    /**
     * Returns whether or not mbstring.func_overload is in effect.
     *
     * @internal You should not use this directly from another application
     *
     * Note: MB_OVERLOAD_STRING === 2, but we don't reference the constant
     * (for nuisance-free PHP 8 support)
     *
     * @return bool
     */
    protected static function isMbStringOverride()
    {
        static $mbstring = null;

        if ($mbstring === null) {
            $mbstring = extension_loaded('mbstring')
                && defined('MB_OVERLOAD_STRING')
                &&
            ((int) (ini_get('mbstring.func_overload')) & 2);
            // MB_OVERLOAD_STRING === 2
        }
        /** @var bool $mbstring */

        return $mbstring;
    }
}
Core/Curve25519/Fe.php000064400000005641152331075510010201 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_Curve25519_Fe', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_Curve25519_Fe
 *
 * This represents a Field Element
 */
class ParagonIE_Sodium_Core_Curve25519_Fe implements ArrayAccess
{
    /**
     * @var array<int, int>
     */
    protected $container = array();

    /**
     * @var int
     */
    protected $size = 10;

    /**
     * @internal You should not use this directly from another application
     *
     * @param array<int, int> $array
     * @param bool $save_indexes
     * @return self
     */
    public static function fromArray($array, $save_indexes = null)
    {
        $count = count($array);
        if ($save_indexes) {
            $keys = array_keys($array);
        } else {
            $keys = range(0, $count - 1);
        }
        $array = array_values($array);
        /** @var array<int, int> $keys */

        $obj = new ParagonIE_Sodium_Core_Curve25519_Fe();
        if ($save_indexes) {
            for ($i = 0; $i < $count; ++$i) {
                $obj->offsetSet($keys[$i], $array[$i]);
            }
        } else {
            for ($i = 0; $i < $count; ++$i) {
                $obj->offsetSet($i, $array[$i]);
            }
        }
        return $obj;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int|null $offset
     * @param int $value
     * @return void
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetSet($offset, $value)
    {
        if (!is_int($value)) {
            throw new InvalidArgumentException('Expected an integer');
        }
        if (is_null($offset)) {
            $this->container[] = $value;
        } else {
            $this->container[$offset] = $value;
        }
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $offset
     * @return bool
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetExists($offset)
    {
        return isset($this->container[$offset]);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $offset
     * @return void
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetUnset($offset)
    {
        unset($this->container[$offset]);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $offset
     * @return int
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetGet($offset)
    {
        if (!isset($this->container[$offset])) {
            $this->container[$offset] = 0;
        }
        return (int) ($this->container[$offset]);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @return array
     */
    public function __debugInfo()
    {
        return array(implode(', ', $this->container));
    }
}
Core/Curve25519/README.md000064400000000332152331075510010405 0ustar00# Curve25519 Data Structures

These are PHP implementation of the [structs used in the ref10 curve25519 code](https://github.com/jedisct1/libsodium/blob/master/src/libsodium/include/sodium/private/curve25519_ref10.h).
Core/Curve25519/Ge/P1p1.php000064400000003201152331075510010711 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_Curve25519_Ge_P1p1', false)) {
    return;
}
/**
 * Class ParagonIE_Sodium_Core_Curve25519_Ge_P1p1
 */
class ParagonIE_Sodium_Core_Curve25519_Ge_P1p1
{
    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $X;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $Y;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $Z;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $T;

    /**
     * ParagonIE_Sodium_Core_Curve25519_Ge_P1p1 constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $x
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $y
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $z
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $t
     */
    public function __construct(
        ParagonIE_Sodium_Core_Curve25519_Fe $x = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $y = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $z = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $t = null
    ) {
        if ($x === null) {
            $x = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->X = $x;
        if ($y === null) {
            $y = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->Y = $y;
        if ($z === null) {
            $z = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->Z = $z;
        if ($t === null) {
            $t = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->T = $t;
    }
}
Core/Curve25519/Ge/Precomp.php000064400000002650152331075510011604 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_Curve25519_Ge_Precomp', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_Curve25519_Ge_Precomp
 */
class ParagonIE_Sodium_Core_Curve25519_Ge_Precomp
{
    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $yplusx;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $yminusx;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $xy2d;

    /**
     * ParagonIE_Sodium_Core_Curve25519_Ge_Precomp constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $yplusx
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $yminusx
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $xy2d
     */
    public function __construct(
        ParagonIE_Sodium_Core_Curve25519_Fe $yplusx = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $yminusx = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $xy2d = null
    ) {
        if ($yplusx === null) {
            $yplusx = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->yplusx = $yplusx;
        if ($yminusx === null) {
            $yminusx = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->yminusx = $yminusx;
        if ($xy2d === null) {
            $xy2d = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->xy2d = $xy2d;
    }
}
Core/Curve25519/Ge/Cached.php000064400000003345152331075510011350 0ustar00<?php


if (class_exists('ParagonIE_Sodium_Core_Curve25519_Ge_Cached', false)) {
    return;
}
/**
 * Class ParagonIE_Sodium_Core_Curve25519_Ge_Cached
 */
class ParagonIE_Sodium_Core_Curve25519_Ge_Cached
{
    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $YplusX;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $YminusX;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $Z;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $T2d;

    /**
     * ParagonIE_Sodium_Core_Curve25519_Ge_Cached constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $YplusX
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $YminusX
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $Z
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $T2d
     */
    public function __construct(
        ParagonIE_Sodium_Core_Curve25519_Fe $YplusX = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $YminusX = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $Z = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $T2d = null
    ) {
        if ($YplusX === null) {
            $YplusX = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->YplusX = $YplusX;
        if ($YminusX === null) {
            $YminusX = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->YminusX = $YminusX;
        if ($Z === null) {
            $Z = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->Z = $Z;
        if ($T2d === null) {
            $T2d = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->T2d = $T2d;
    }
}
Core/Curve25519/Ge/P3.php000064400000003172152331075510010461 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_Curve25519_Ge_P3', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_Curve25519_Ge_P3
 */
class ParagonIE_Sodium_Core_Curve25519_Ge_P3
{
    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $X;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $Y;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $Z;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $T;

    /**
     * ParagonIE_Sodium_Core_Curve25519_Ge_P3 constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $x
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $y
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $z
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $t
     */
    public function __construct(
        ParagonIE_Sodium_Core_Curve25519_Fe $x = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $y = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $z = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $t = null
    ) {
        if ($x === null) {
            $x = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->X = $x;
        if ($y === null) {
            $y = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->Y = $y;
        if ($z === null) {
            $z = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->Z = $z;
        if ($t === null) {
            $t = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->T = $t;
    }
}
Core/Curve25519/Ge/P2.php000064400000002501152331075510010453 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_Curve25519_Ge_P2', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_Curve25519_Ge_P2
 */
class ParagonIE_Sodium_Core_Curve25519_Ge_P2
{
    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $X;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $Y;

    /**
     * @var ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public $Z;

    /**
     * ParagonIE_Sodium_Core_Curve25519_Ge_P2 constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $x
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $y
     * @param ParagonIE_Sodium_Core_Curve25519_Fe|null $z
     */
    public function __construct(
        ParagonIE_Sodium_Core_Curve25519_Fe $x = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $y = null,
        ParagonIE_Sodium_Core_Curve25519_Fe $z = null
    ) {
        if ($x === null) {
            $x = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->X = $x;
        if ($y === null) {
            $y = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->Y = $y;
        if ($z === null) {
            $z = new ParagonIE_Sodium_Core_Curve25519_Fe();
        }
        $this->Z = $z;
    }
}
Core/Curve25519/H.php000064400000327571152331075510010047 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_Curve25519_H', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_Curve25519_H
 *
 * This just contains the constants in the ref10/base.h file
 */
class ParagonIE_Sodium_Core_Curve25519_H extends ParagonIE_Sodium_Core_Util
{
    /**
     * See: libsodium's crypto_core/curve25519/ref10/base.h
     *
     * @var array<int, array<int, array<int, array<int, int>>>> Basically, int[32][8][3][10]
     */
    protected static $base = array(
        array(
            array(
                array(25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605),
                array(-12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378),
                array(-8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546),
            ),
            array(
                array(-12815894, -12976347, -21581243, 11784320, -25355658, -2750717, -11717903, -3814571, -358445, -10211303),
                array(-21703237, 6903825, 27185491, 6451973, -29577724, -9554005, -15616551, 11189268, -26829678, -5319081),
                array(26966642, 11152617, 32442495, 15396054, 14353839, -12752335, -3128826, -9541118, -15472047, -4166697),
            ),
            array(
                array(15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024),
                array(16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574),
                array(30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357),
            ),
            array(
                array(-17036878, 13921892, 10945806, -6033431, 27105052, -16084379, -28926210, 15006023, 3284568, -6276540),
                array(23599295, -8306047, -11193664, -7687416, 13236774, 10506355, 7464579, 9656445, 13059162, 10374397),
                array(7798556, 16710257, 3033922, 2874086, 28997861, 2835604, 32406664, -3839045, -641708, -101325),
            ),
            array(
                array(10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380),
                array(4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306),
                array(19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942),
            ),
            array(
                array(-15371964, -12862754, 32573250, 4720197, -26436522, 5875511, -19188627, -15224819, -9818940, -12085777),
                array(-8549212, 109983, 15149363, 2178705, 22900618, 4543417, 3044240, -15689887, 1762328, 14866737),
                array(-18199695, -15951423, -10473290, 1707278, -17185920, 3916101, -28236412, 3959421, 27914454, 4383652),
            ),
            array(
                array(5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766),
                array(-30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701),
                array(28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300),
            ),
            array(
                array(14499471, -2729599, -33191113, -4254652, 28494862, 14271267, 30290735, 10876454, -33154098, 2381726),
                array(-7195431, -2655363, -14730155, 462251, -27724326, 3941372, -6236617, 3696005, -32300832, 15351955),
                array(27431194, 8222322, 16448760, -3907995, -18707002, 11938355, -32961401, -2970515, 29551813, 10109425),
            ),
        ),
        array(
            array(
                array(-13657040, -13155431, -31283750, 11777098, 21447386, 6519384, -2378284, -1627556, 10092783, -4764171),
                array(27939166, 14210322, 4677035, 16277044, -22964462, -12398139, -32508754, 12005538, -17810127, 12803510),
                array(17228999, -15661624, -1233527, 300140, -1224870, -11714777, 30364213, -9038194, 18016357, 4397660),
            ),
            array(
                array(-10958843, -7690207, 4776341, -14954238, 27850028, -15602212, -26619106, 14544525, -17477504, 982639),
                array(29253598, 15796703, -2863982, -9908884, 10057023, 3163536, 7332899, -4120128, -21047696, 9934963),
                array(5793303, 16271923, -24131614, -10116404, 29188560, 1206517, -14747930, 4559895, -30123922, -10897950),
            ),
            array(
                array(-27643952, -11493006, 16282657, -11036493, 28414021, -15012264, 24191034, 4541697, -13338309, 5500568),
                array(12650548, -1497113, 9052871, 11355358, -17680037, -8400164, -17430592, 12264343, 10874051, 13524335),
                array(25556948, -3045990, 714651, 2510400, 23394682, -10415330, 33119038, 5080568, -22528059, 5376628),
            ),
            array(
                array(-26088264, -4011052, -17013699, -3537628, -6726793, 1920897, -22321305, -9447443, 4535768, 1569007),
                array(-2255422, 14606630, -21692440, -8039818, 28430649, 8775819, -30494562, 3044290, 31848280, 12543772),
                array(-22028579, 2943893, -31857513, 6777306, 13784462, -4292203, -27377195, -2062731, 7718482, 14474653),
            ),
            array(
                array(2385315, 2454213, -22631320, 46603, -4437935, -15680415, 656965, -7236665, 24316168, -5253567),
                array(13741529, 10911568, -33233417, -8603737, -20177830, -1033297, 33040651, -13424532, -20729456, 8321686),
                array(21060490, -2212744, 15712757, -4336099, 1639040, 10656336, 23845965, -11874838, -9984458, 608372),
            ),
            array(
                array(-13672732, -15087586, -10889693, -7557059, -6036909, 11305547, 1123968, -6780577, 27229399, 23887),
                array(-23244140, -294205, -11744728, 14712571, -29465699, -2029617, 12797024, -6440308, -1633405, 16678954),
                array(-29500620, 4770662, -16054387, 14001338, 7830047, 9564805, -1508144, -4795045, -17169265, 4904953),
            ),
            array(
                array(24059557, 14617003, 19037157, -15039908, 19766093, -14906429, 5169211, 16191880, 2128236, -4326833),
                array(-16981152, 4124966, -8540610, -10653797, 30336522, -14105247, -29806336, 916033, -6882542, -2986532),
                array(-22630907, 12419372, -7134229, -7473371, -16478904, 16739175, 285431, 2763829, 15736322, 4143876),
            ),
            array(
                array(2379352, 11839345, -4110402, -5988665, 11274298, 794957, 212801, -14594663, 23527084, -16458268),
                array(33431127, -11130478, -17838966, -15626900, 8909499, 8376530, -32625340, 4087881, -15188911, -14416214),
                array(1767683, 7197987, -13205226, -2022635, -13091350, 448826, 5799055, 4357868, -4774191, -16323038),
            ),
        ),
        array(
            array(
                array(6721966, 13833823, -23523388, -1551314, 26354293, -11863321, 23365147, -3949732, 7390890, 2759800),
                array(4409041, 2052381, 23373853, 10530217, 7676779, -12885954, 21302353, -4264057, 1244380, -12919645),
                array(-4421239, 7169619, 4982368, -2957590, 30256825, -2777540, 14086413, 9208236, 15886429, 16489664),
            ),
            array(
                array(1996075, 10375649, 14346367, 13311202, -6874135, -16438411, -13693198, 398369, -30606455, -712933),
                array(-25307465, 9795880, -2777414, 14878809, -33531835, 14780363, 13348553, 12076947, -30836462, 5113182),
                array(-17770784, 11797796, 31950843, 13929123, -25888302, 12288344, -30341101, -7336386, 13847711, 5387222),
            ),
            array(
                array(-18582163, -3416217, 17824843, -2340966, 22744343, -10442611, 8763061, 3617786, -19600662, 10370991),
                array(20246567, -14369378, 22358229, -543712, 18507283, -10413996, 14554437, -8746092, 32232924, 16763880),
                array(9648505, 10094563, 26416693, 14745928, -30374318, -6472621, 11094161, 15689506, 3140038, -16510092),
            ),
            array(
                array(-16160072, 5472695, 31895588, 4744994, 8823515, 10365685, -27224800, 9448613, -28774454, 366295),
                array(19153450, 11523972, -11096490, -6503142, -24647631, 5420647, 28344573, 8041113, 719605, 11671788),
                array(8678025, 2694440, -6808014, 2517372, 4964326, 11152271, -15432916, -15266516, 27000813, -10195553),
            ),
            array(
                array(-15157904, 7134312, 8639287, -2814877, -7235688, 10421742, 564065, 5336097, 6750977, -14521026),
                array(11836410, -3979488, 26297894, 16080799, 23455045, 15735944, 1695823, -8819122, 8169720, 16220347),
                array(-18115838, 8653647, 17578566, -6092619, -8025777, -16012763, -11144307, -2627664, -5990708, -14166033),
            ),
            array(
                array(-23308498, -10968312, 15213228, -10081214, -30853605, -11050004, 27884329, 2847284, 2655861, 1738395),
                array(-27537433, -14253021, -25336301, -8002780, -9370762, 8129821, 21651608, -3239336, -19087449, -11005278),
                array(1533110, 3437855, 23735889, 459276, 29970501, 11335377, 26030092, 5821408, 10478196, 8544890),
            ),
            array(
                array(32173121, -16129311, 24896207, 3921497, 22579056, -3410854, 19270449, 12217473, 17789017, -3395995),
                array(-30552961, -2228401, -15578829, -10147201, 13243889, 517024, 15479401, -3853233, 30460520, 1052596),
                array(-11614875, 13323618, 32618793, 8175907, -15230173, 12596687, 27491595, -4612359, 3179268, -9478891),
            ),
            array(
                array(31947069, -14366651, -4640583, -15339921, -15125977, -6039709, -14756777, -16411740, 19072640, -9511060),
                array(11685058, 11822410, 3158003, -13952594, 33402194, -4165066, 5977896, -5215017, 473099, 5040608),
                array(-20290863, 8198642, -27410132, 11602123, 1290375, -2799760, 28326862, 1721092, -19558642, -3131606),
            ),
        ),
        array(
            array(
                array(7881532, 10687937, 7578723, 7738378, -18951012, -2553952, 21820786, 8076149, -27868496, 11538389),
                array(-19935666, 3899861, 18283497, -6801568, -15728660, -11249211, 8754525, 7446702, -5676054, 5797016),
                array(-11295600, -3793569, -15782110, -7964573, 12708869, -8456199, 2014099, -9050574, -2369172, -5877341),
            ),
            array(
                array(-22472376, -11568741, -27682020, 1146375, 18956691, 16640559, 1192730, -3714199, 15123619, 10811505),
                array(14352098, -3419715, -18942044, 10822655, 32750596, 4699007, -70363, 15776356, -28886779, -11974553),
                array(-28241164, -8072475, -4978962, -5315317, 29416931, 1847569, -20654173, -16484855, 4714547, -9600655),
            ),
            array(
                array(15200332, 8368572, 19679101, 15970074, -31872674, 1959451, 24611599, -4543832, -11745876, 12340220),
                array(12876937, -10480056, 33134381, 6590940, -6307776, 14872440, 9613953, 8241152, 15370987, 9608631),
                array(-4143277, -12014408, 8446281, -391603, 4407738, 13629032, -7724868, 15866074, -28210621, -8814099),
            ),
            array(
                array(26660628, -15677655, 8393734, 358047, -7401291, 992988, -23904233, 858697, 20571223, 8420556),
                array(14620715, 13067227, -15447274, 8264467, 14106269, 15080814, 33531827, 12516406, -21574435, -12476749),
                array(236881, 10476226, 57258, -14677024, 6472998, 2466984, 17258519, 7256740, 8791136, 15069930),
            ),
            array(
                array(1276410, -9371918, 22949635, -16322807, -23493039, -5702186, 14711875, 4874229, -30663140, -2331391),
                array(5855666, 4990204, -13711848, 7294284, -7804282, 1924647, -1423175, -7912378, -33069337, 9234253),
                array(20590503, -9018988, 31529744, -7352666, -2706834, 10650548, 31559055, -11609587, 18979186, 13396066),
            ),
            array(
                array(24474287, 4968103, 22267082, 4407354, 24063882, -8325180, -18816887, 13594782, 33514650, 7021958),
                array(-11566906, -6565505, -21365085, 15928892, -26158305, 4315421, -25948728, -3916677, -21480480, 12868082),
                array(-28635013, 13504661, 19988037, -2132761, 21078225, 6443208, -21446107, 2244500, -12455797, -8089383),
            ),
            array(
                array(-30595528, 13793479, -5852820, 319136, -25723172, -6263899, 33086546, 8957937, -15233648, 5540521),
                array(-11630176, -11503902, -8119500, -7643073, 2620056, 1022908, -23710744, -1568984, -16128528, -14962807),
                array(23152971, 775386, 27395463, 14006635, -9701118, 4649512, 1689819, 892185, -11513277, -15205948),
            ),
            array(
                array(9770129, 9586738, 26496094, 4324120, 1556511, -3550024, 27453819, 4763127, -19179614, 5867134),
                array(-32765025, 1927590, 31726409, -4753295, 23962434, -16019500, 27846559, 5931263, -29749703, -16108455),
                array(27461885, -2977536, 22380810, 1815854, -23033753, -3031938, 7283490, -15148073, -19526700, 7734629),
            ),
        ),
        array(
            array(
                array(-8010264, -9590817, -11120403, 6196038, 29344158, -13430885, 7585295, -3176626, 18549497, 15302069),
                array(-32658337, -6171222, -7672793, -11051681, 6258878, 13504381, 10458790, -6418461, -8872242, 8424746),
                array(24687205, 8613276, -30667046, -3233545, 1863892, -1830544, 19206234, 7134917, -11284482, -828919),
            ),
            array(
                array(11334899, -9218022, 8025293, 12707519, 17523892, -10476071, 10243738, -14685461, -5066034, 16498837),
                array(8911542, 6887158, -9584260, -6958590, 11145641, -9543680, 17303925, -14124238, 6536641, 10543906),
                array(-28946384, 15479763, -17466835, 568876, -1497683, 11223454, -2669190, -16625574, -27235709, 8876771),
            ),
            array(
                array(-25742899, -12566864, -15649966, -846607, -33026686, -796288, -33481822, 15824474, -604426, -9039817),
                array(10330056, 70051, 7957388, -9002667, 9764902, 15609756, 27698697, -4890037, 1657394, 3084098),
                array(10477963, -7470260, 12119566, -13250805, 29016247, -5365589, 31280319, 14396151, -30233575, 15272409),
            ),
            array(
                array(-12288309, 3169463, 28813183, 16658753, 25116432, -5630466, -25173957, -12636138, -25014757, 1950504),
                array(-26180358, 9489187, 11053416, -14746161, -31053720, 5825630, -8384306, -8767532, 15341279, 8373727),
                array(28685821, 7759505, -14378516, -12002860, -31971820, 4079242, 298136, -10232602, -2878207, 15190420),
            ),
            array(
                array(-32932876, 13806336, -14337485, -15794431, -24004620, 10940928, 8669718, 2742393, -26033313, -6875003),
                array(-1580388, -11729417, -25979658, -11445023, -17411874, -10912854, 9291594, -16247779, -12154742, 6048605),
                array(-30305315, 14843444, 1539301, 11864366, 20201677, 1900163, 13934231, 5128323, 11213262, 9168384),
            ),
            array(
                array(-26280513, 11007847, 19408960, -940758, -18592965, -4328580, -5088060, -11105150, 20470157, -16398701),
                array(-23136053, 9282192, 14855179, -15390078, -7362815, -14408560, -22783952, 14461608, 14042978, 5230683),
                array(29969567, -2741594, -16711867, -8552442, 9175486, -2468974, 21556951, 3506042, -5933891, -12449708),
            ),
            array(
                array(-3144746, 8744661, 19704003, 4581278, -20430686, 6830683, -21284170, 8971513, -28539189, 15326563),
                array(-19464629, 10110288, -17262528, -3503892, -23500387, 1355669, -15523050, 15300988, -20514118, 9168260),
                array(-5353335, 4488613, -23803248, 16314347, 7780487, -15638939, -28948358, 9601605, 33087103, -9011387),
            ),
            array(
                array(-19443170, -15512900, -20797467, -12445323, -29824447, 10229461, -27444329, -15000531, -5996870, 15664672),
                array(23294591, -16632613, -22650781, -8470978, 27844204, 11461195, 13099750, -2460356, 18151676, 13417686),
                array(-24722913, -4176517, -31150679, 5988919, -26858785, 6685065, 1661597, -12551441, 15271676, -15452665),
            ),
        ),
        array(
            array(
                array(11433042, -13228665, 8239631, -5279517, -1985436, -725718, -18698764, 2167544, -6921301, -13440182),
                array(-31436171, 15575146, 30436815, 12192228, -22463353, 9395379, -9917708, -8638997, 12215110, 12028277),
                array(14098400, 6555944, 23007258, 5757252, -15427832, -12950502, 30123440, 4617780, -16900089, -655628),
            ),
            array(
                array(-4026201, -15240835, 11893168, 13718664, -14809462, 1847385, -15819999, 10154009, 23973261, -12684474),
                array(-26531820, -3695990, -1908898, 2534301, -31870557, -16550355, 18341390, -11419951, 32013174, -10103539),
                array(-25479301, 10876443, -11771086, -14625140, -12369567, 1838104, 21911214, 6354752, 4425632, -837822),
            ),
            array(
                array(-10433389, -14612966, 22229858, -3091047, -13191166, 776729, -17415375, -12020462, 4725005, 14044970),
                array(19268650, -7304421, 1555349, 8692754, -21474059, -9910664, 6347390, -1411784, -19522291, -16109756),
                array(-24864089, 12986008, -10898878, -5558584, -11312371, -148526, 19541418, 8180106, 9282262, 10282508),
            ),
            array(
                array(-26205082, 4428547, -8661196, -13194263, 4098402, -14165257, 15522535, 8372215, 5542595, -10702683),
                array(-10562541, 14895633, 26814552, -16673850, -17480754, -2489360, -2781891, 6993761, -18093885, 10114655),
                array(-20107055, -929418, 31422704, 10427861, -7110749, 6150669, -29091755, -11529146, 25953725, -106158),
            ),
            array(
                array(-4234397, -8039292, -9119125, 3046000, 2101609, -12607294, 19390020, 6094296, -3315279, 12831125),
                array(-15998678, 7578152, 5310217, 14408357, -33548620, -224739, 31575954, 6326196, 7381791, -2421839),
                array(-20902779, 3296811, 24736065, -16328389, 18374254, 7318640, 6295303, 8082724, -15362489, 12339664),
            ),
            array(
                array(27724736, 2291157, 6088201, -14184798, 1792727, 5857634, 13848414, 15768922, 25091167, 14856294),
                array(-18866652, 8331043, 24373479, 8541013, -701998, -9269457, 12927300, -12695493, -22182473, -9012899),
                array(-11423429, -5421590, 11632845, 3405020, 30536730, -11674039, -27260765, 13866390, 30146206, 9142070),
            ),
            array(
                array(3924129, -15307516, -13817122, -10054960, 12291820, -668366, -27702774, 9326384, -8237858, 4171294),
                array(-15921940, 16037937, 6713787, 16606682, -21612135, 2790944, 26396185, 3731949, 345228, -5462949),
                array(-21327538, 13448259, 25284571, 1143661, 20614966, -8849387, 2031539, -12391231, -16253183, -13582083),
            ),
            array(
                array(31016211, -16722429, 26371392, -14451233, -5027349, 14854137, 17477601, 3842657, 28012650, -16405420),
                array(-5075835, 9368966, -8562079, -4600902, -15249953, 6970560, -9189873, 16292057, -8867157, 3507940),
                array(29439664, 3537914, 23333589, 6997794, -17555561, -11018068, -15209202, -15051267, -9164929, 6580396),
            ),
        ),
        array(
            array(
                array(-12185861, -7679788, 16438269, 10826160, -8696817, -6235611, 17860444, -9273846, -2095802, 9304567),
                array(20714564, -4336911, 29088195, 7406487, 11426967, -5095705, 14792667, -14608617, 5289421, -477127),
                array(-16665533, -10650790, -6160345, -13305760, 9192020, -1802462, 17271490, 12349094, 26939669, -3752294),
            ),
            array(
                array(-12889898, 9373458, 31595848, 16374215, 21471720, 13221525, -27283495, -12348559, -3698806, 117887),
                array(22263325, -6560050, 3984570, -11174646, -15114008, -566785, 28311253, 5358056, -23319780, 541964),
                array(16259219, 3261970, 2309254, -15534474, -16885711, -4581916, 24134070, -16705829, -13337066, -13552195),
            ),
            array(
                array(9378160, -13140186, -22845982, -12745264, 28198281, -7244098, -2399684, -717351, 690426, 14876244),
                array(24977353, -314384, -8223969, -13465086, 28432343, -1176353, -13068804, -12297348, -22380984, 6618999),
                array(-1538174, 11685646, 12944378, 13682314, -24389511, -14413193, 8044829, -13817328, 32239829, -5652762),
            ),
            array(
                array(-18603066, 4762990, -926250, 8885304, -28412480, -3187315, 9781647, -10350059, 32779359, 5095274),
                array(-33008130, -5214506, -32264887, -3685216, 9460461, -9327423, -24601656, 14506724, 21639561, -2630236),
                array(-16400943, -13112215, 25239338, 15531969, 3987758, -4499318, -1289502, -6863535, 17874574, 558605),
            ),
            array(
                array(-13600129, 10240081, 9171883, 16131053, -20869254, 9599700, 33499487, 5080151, 2085892, 5119761),
                array(-22205145, -2519528, -16381601, 414691, -25019550, 2170430, 30634760, -8363614, -31999993, -5759884),
                array(-6845704, 15791202, 8550074, -1312654, 29928809, -12092256, 27534430, -7192145, -22351378, 12961482),
            ),
            array(
                array(-24492060, -9570771, 10368194, 11582341, -23397293, -2245287, 16533930, 8206996, -30194652, -5159638),
                array(-11121496, -3382234, 2307366, 6362031, -135455, 8868177, -16835630, 7031275, 7589640, 8945490),
                array(-32152748, 8917967, 6661220, -11677616, -1192060, -15793393, 7251489, -11182180, 24099109, -14456170),
            ),
            array(
                array(5019558, -7907470, 4244127, -14714356, -26933272, 6453165, -19118182, -13289025, -6231896, -10280736),
                array(10853594, 10721687, 26480089, 5861829, -22995819, 1972175, -1866647, -10557898, -3363451, -6441124),
                array(-17002408, 5906790, 221599, -6563147, 7828208, -13248918, 24362661, -2008168, -13866408, 7421392),
            ),
            array(
                array(8139927, -6546497, 32257646, -5890546, 30375719, 1886181, -21175108, 15441252, 28826358, -4123029),
                array(6267086, 9695052, 7709135, -16603597, -32869068, -1886135, 14795160, -7840124, 13746021, -1742048),
                array(28584902, 7787108, -6732942, -15050729, 22846041, -7571236, -3181936, -363524, 4771362, -8419958),
            ),
        ),
        array(
            array(
                array(24949256, 6376279, -27466481, -8174608, -18646154, -9930606, 33543569, -12141695, 3569627, 11342593),
                array(26514989, 4740088, 27912651, 3697550, 19331575, -11472339, 6809886, 4608608, 7325975, -14801071),
                array(-11618399, -14554430, -24321212, 7655128, -1369274, 5214312, -27400540, 10258390, -17646694, -8186692),
            ),
            array(
                array(11431204, 15823007, 26570245, 14329124, 18029990, 4796082, -31446179, 15580664, 9280358, -3973687),
                array(-160783, -10326257, -22855316, -4304997, -20861367, -13621002, -32810901, -11181622, -15545091, 4387441),
                array(-20799378, 12194512, 3937617, -5805892, -27154820, 9340370, -24513992, 8548137, 20617071, -7482001),
            ),
            array(
                array(-938825, -3930586, -8714311, 16124718, 24603125, -6225393, -13775352, -11875822, 24345683, 10325460),
                array(-19855277, -1568885, -22202708, 8714034, 14007766, 6928528, 16318175, -1010689, 4766743, 3552007),
                array(-21751364, -16730916, 1351763, -803421, -4009670, 3950935, 3217514, 14481909, 10988822, -3994762),
            ),
            array(
                array(15564307, -14311570, 3101243, 5684148, 30446780, -8051356, 12677127, -6505343, -8295852, 13296005),
                array(-9442290, 6624296, -30298964, -11913677, -4670981, -2057379, 31521204, 9614054, -30000824, 12074674),
                array(4771191, -135239, 14290749, -13089852, 27992298, 14998318, -1413936, -1556716, 29832613, -16391035),
            ),
            array(
                array(7064884, -7541174, -19161962, -5067537, -18891269, -2912736, 25825242, 5293297, -27122660, 13101590),
                array(-2298563, 2439670, -7466610, 1719965, -27267541, -16328445, 32512469, -5317593, -30356070, -4190957),
                array(-30006540, 10162316, -33180176, 3981723, -16482138, -13070044, 14413974, 9515896, 19568978, 9628812),
            ),
            array(
                array(33053803, 199357, 15894591, 1583059, 27380243, -4580435, -17838894, -6106839, -6291786, 3437740),
                array(-18978877, 3884493, 19469877, 12726490, 15913552, 13614290, -22961733, 70104, 7463304, 4176122),
                array(-27124001, 10659917, 11482427, -16070381, 12771467, -6635117, -32719404, -5322751, 24216882, 5944158),
            ),
            array(
                array(8894125, 7450974, -2664149, -9765752, -28080517, -12389115, 19345746, 14680796, 11632993, 5847885),
                array(26942781, -2315317, 9129564, -4906607, 26024105, 11769399, -11518837, 6367194, -9727230, 4782140),
                array(19916461, -4828410, -22910704, -11414391, 25606324, -5972441, 33253853, 8220911, 6358847, -1873857),
            ),
            array(
                array(801428, -2081702, 16569428, 11065167, 29875704, 96627, 7908388, -4480480, -13538503, 1387155),
                array(19646058, 5720633, -11416706, 12814209, 11607948, 12749789, 14147075, 15156355, -21866831, 11835260),
                array(19299512, 1155910, 28703737, 14890794, 2925026, 7269399, 26121523, 15467869, -26560550, 5052483),
            ),
        ),
        array(
            array(
                array(-3017432, 10058206, 1980837, 3964243, 22160966, 12322533, -6431123, -12618185, 12228557, -7003677),
                array(32944382, 14922211, -22844894, 5188528, 21913450, -8719943, 4001465, 13238564, -6114803, 8653815),
                array(22865569, -4652735, 27603668, -12545395, 14348958, 8234005, 24808405, 5719875, 28483275, 2841751),
            ),
            array(
                array(-16420968, -1113305, -327719, -12107856, 21886282, -15552774, -1887966, -315658, 19932058, -12739203),
                array(-11656086, 10087521, -8864888, -5536143, -19278573, -3055912, 3999228, 13239134, -4777469, -13910208),
                array(1382174, -11694719, 17266790, 9194690, -13324356, 9720081, 20403944, 11284705, -14013818, 3093230),
            ),
            array(
                array(16650921, -11037932, -1064178, 1570629, -8329746, 7352753, -302424, 16271225, -24049421, -6691850),
                array(-21911077, -5927941, -4611316, -5560156, -31744103, -10785293, 24123614, 15193618, -21652117, -16739389),
                array(-9935934, -4289447, -25279823, 4372842, 2087473, 10399484, 31870908, 14690798, 17361620, 11864968),
            ),
            array(
                array(-11307610, 6210372, 13206574, 5806320, -29017692, -13967200, -12331205, -7486601, -25578460, -16240689),
                array(14668462, -12270235, 26039039, 15305210, 25515617, 4542480, 10453892, 6577524, 9145645, -6443880),
                array(5974874, 3053895, -9433049, -10385191, -31865124, 3225009, -7972642, 3936128, -5652273, -3050304),
            ),
            array(
                array(30625386, -4729400, -25555961, -12792866, -20484575, 7695099, 17097188, -16303496, -27999779, 1803632),
                array(-3553091, 9865099, -5228566, 4272701, -5673832, -16689700, 14911344, 12196514, -21405489, 7047412),
                array(20093277, 9920966, -11138194, -5343857, 13161587, 12044805, -32856851, 4124601, -32343828, -10257566),
            ),
            array(
                array(-20788824, 14084654, -13531713, 7842147, 19119038, -13822605, 4752377, -8714640, -21679658, 2288038),
                array(-26819236, -3283715, 29965059, 3039786, -14473765, 2540457, 29457502, 14625692, -24819617, 12570232),
                array(-1063558, -11551823, 16920318, 12494842, 1278292, -5869109, -21159943, -3498680, -11974704, 4724943),
            ),
            array(
                array(17960970, -11775534, -4140968, -9702530, -8876562, -1410617, -12907383, -8659932, -29576300, 1903856),
                array(23134274, -14279132, -10681997, -1611936, 20684485, 15770816, -12989750, 3190296, 26955097, 14109738),
                array(15308788, 5320727, -30113809, -14318877, 22902008, 7767164, 29425325, -11277562, 31960942, 11934971),
            ),
            array(
                array(-27395711, 8435796, 4109644, 12222639, -24627868, 14818669, 20638173, 4875028, 10491392, 1379718),
                array(-13159415, 9197841, 3875503, -8936108, -1383712, -5879801, 33518459, 16176658, 21432314, 12180697),
                array(-11787308, 11500838, 13787581, -13832590, -22430679, 10140205, 1465425, 12689540, -10301319, -13872883),
            ),
        ),
        array(
            array(
                array(5414091, -15386041, -21007664, 9643570, 12834970, 1186149, -2622916, -1342231, 26128231, 6032912),
                array(-26337395, -13766162, 32496025, -13653919, 17847801, -12669156, 3604025, 8316894, -25875034, -10437358),
                array(3296484, 6223048, 24680646, -12246460, -23052020, 5903205, -8862297, -4639164, 12376617, 3188849),
            ),
            array(
                array(29190488, -14659046, 27549113, -1183516, 3520066, -10697301, 32049515, -7309113, -16109234, -9852307),
                array(-14744486, -9309156, 735818, -598978, -20407687, -5057904, 25246078, -15795669, 18640741, -960977),
                array(-6928835, -16430795, 10361374, 5642961, 4910474, 12345252, -31638386, -494430, 10530747, 1053335),
            ),
            array(
                array(-29265967, -14186805, -13538216, -12117373, -19457059, -10655384, -31462369, -2948985, 24018831, 15026644),
                array(-22592535, -3145277, -2289276, 5953843, -13440189, 9425631, 25310643, 13003497, -2314791, -15145616),
                array(-27419985, -603321, -8043984, -1669117, -26092265, 13987819, -27297622, 187899, -23166419, -2531735),
            ),
            array(
                array(-21744398, -13810475, 1844840, 5021428, -10434399, -15911473, 9716667, 16266922, -5070217, 726099),
                array(29370922, -6053998, 7334071, -15342259, 9385287, 2247707, -13661962, -4839461, 30007388, -15823341),
                array(-936379, 16086691, 23751945, -543318, -1167538, -5189036, 9137109, 730663, 9835848, 4555336),
            ),
            array(
                array(-23376435, 1410446, -22253753, -12899614, 30867635, 15826977, 17693930, 544696, -11985298, 12422646),
                array(31117226, -12215734, -13502838, 6561947, -9876867, -12757670, -5118685, -4096706, 29120153, 13924425),
                array(-17400879, -14233209, 19675799, -2734756, -11006962, -5858820, -9383939, -11317700, 7240931, -237388),
            ),
            array(
                array(-31361739, -11346780, -15007447, -5856218, -22453340, -12152771, 1222336, 4389483, 3293637, -15551743),
                array(-16684801, -14444245, 11038544, 11054958, -13801175, -3338533, -24319580, 7733547, 12796905, -6335822),
                array(-8759414, -10817836, -25418864, 10783769, -30615557, -9746811, -28253339, 3647836, 3222231, -11160462),
            ),
            array(
                array(18606113, 1693100, -25448386, -15170272, 4112353, 10045021, 23603893, -2048234, -7550776, 2484985),
                array(9255317, -3131197, -12156162, -1004256, 13098013, -9214866, 16377220, -2102812, -19802075, -3034702),
                array(-22729289, 7496160, -5742199, 11329249, 19991973, -3347502, -31718148, 9936966, -30097688, -10618797),
            ),
            array(
                array(21878590, -5001297, 4338336, 13643897, -3036865, 13160960, 19708896, 5415497, -7360503, -4109293),
                array(27736861, 10103576, 12500508, 8502413, -3413016, -9633558, 10436918, -1550276, -23659143, -8132100),
                array(19492550, -12104365, -29681976, -852630, -3208171, 12403437, 30066266, 8367329, 13243957, 8709688),
            ),
        ),
        array(
            array(
                array(12015105, 2801261, 28198131, 10151021, 24818120, -4743133, -11194191, -5645734, 5150968, 7274186),
                array(2831366, -12492146, 1478975, 6122054, 23825128, -12733586, 31097299, 6083058, 31021603, -9793610),
                array(-2529932, -2229646, 445613, 10720828, -13849527, -11505937, -23507731, 16354465, 15067285, -14147707),
            ),
            array(
                array(7840942, 14037873, -33364863, 15934016, -728213, -3642706, 21403988, 1057586, -19379462, -12403220),
                array(915865, -16469274, 15608285, -8789130, -24357026, 6060030, -17371319, 8410997, -7220461, 16527025),
                array(32922597, -556987, 20336074, -16184568, 10903705, -5384487, 16957574, 52992, 23834301, 6588044),
            ),
            array(
                array(32752030, 11232950, 3381995, -8714866, 22652988, -10744103, 17159699, 16689107, -20314580, -1305992),
                array(-4689649, 9166776, -25710296, -10847306, 11576752, 12733943, 7924251, -2752281, 1976123, -7249027),
                array(21251222, 16309901, -2983015, -6783122, 30810597, 12967303, 156041, -3371252, 12331345, -8237197),
            ),
            array(
                array(8651614, -4477032, -16085636, -4996994, 13002507, 2950805, 29054427, -5106970, 10008136, -4667901),
                array(31486080, 15114593, -14261250, 12951354, 14369431, -7387845, 16347321, -13662089, 8684155, -10532952),
                array(19443825, 11385320, 24468943, -9659068, -23919258, 2187569, -26263207, -6086921, 31316348, 14219878),
            ),
            array(
                array(-28594490, 1193785, 32245219, 11392485, 31092169, 15722801, 27146014, 6992409, 29126555, 9207390),
                array(32382935, 1110093, 18477781, 11028262, -27411763, -7548111, -4980517, 10843782, -7957600, -14435730),
                array(2814918, 7836403, 27519878, -7868156, -20894015, -11553689, -21494559, 8550130, 28346258, 1994730),
            ),
            array(
                array(-19578299, 8085545, -14000519, -3948622, 2785838, -16231307, -19516951, 7174894, 22628102, 8115180),
                array(-30405132, 955511, -11133838, -15078069, -32447087, -13278079, -25651578, 3317160, -9943017, 930272),
                array(-15303681, -6833769, 28856490, 1357446, 23421993, 1057177, 24091212, -1388970, -22765376, -10650715),
            ),
            array(
                array(-22751231, -5303997, -12907607, -12768866, -15811511, -7797053, -14839018, -16554220, -1867018, 8398970),
                array(-31969310, 2106403, -4736360, 1362501, 12813763, 16200670, 22981545, -6291273, 18009408, -15772772),
                array(-17220923, -9545221, -27784654, 14166835, 29815394, 7444469, 29551787, -3727419, 19288549, 1325865),
            ),
            array(
                array(15100157, -15835752, -23923978, -1005098, -26450192, 15509408, 12376730, -3479146, 33166107, -8042750),
                array(20909231, 13023121, -9209752, 16251778, -5778415, -8094914, 12412151, 10018715, 2213263, -13878373),
                array(32529814, -11074689, 30361439, -16689753, -9135940, 1513226, 22922121, 6382134, -5766928, 8371348),
            ),
        ),
        array(
            array(
                array(9923462, 11271500, 12616794, 3544722, -29998368, -1721626, 12891687, -8193132, -26442943, 10486144),
                array(-22597207, -7012665, 8587003, -8257861, 4084309, -12970062, 361726, 2610596, -23921530, -11455195),
                array(5408411, -1136691, -4969122, 10561668, 24145918, 14240566, 31319731, -4235541, 19985175, -3436086),
            ),
            array(
                array(-13994457, 16616821, 14549246, 3341099, 32155958, 13648976, -17577068, 8849297, 65030, 8370684),
                array(-8320926, -12049626, 31204563, 5839400, -20627288, -1057277, -19442942, 6922164, 12743482, -9800518),
                array(-2361371, 12678785, 28815050, 4759974, -23893047, 4884717, 23783145, 11038569, 18800704, 255233),
            ),
            array(
                array(-5269658, -1773886, 13957886, 7990715, 23132995, 728773, 13393847, 9066957, 19258688, -14753793),
                array(-2936654, -10827535, -10432089, 14516793, -3640786, 4372541, -31934921, 2209390, -1524053, 2055794),
                array(580882, 16705327, 5468415, -2683018, -30926419, -14696000, -7203346, -8994389, -30021019, 7394435),
            ),
            array(
                array(23838809, 1822728, -15738443, 15242727, 8318092, -3733104, -21672180, -3492205, -4821741, 14799921),
                array(13345610, 9759151, 3371034, -16137791, 16353039, 8577942, 31129804, 13496856, -9056018, 7402518),
                array(2286874, -4435931, -20042458, -2008336, -13696227, 5038122, 11006906, -15760352, 8205061, 1607563),
            ),
            array(
                array(14414086, -8002132, 3331830, -3208217, 22249151, -5594188, 18364661, -2906958, 30019587, -9029278),
                array(-27688051, 1585953, -10775053, 931069, -29120221, -11002319, -14410829, 12029093, 9944378, 8024),
                array(4368715, -3709630, 29874200, -15022983, -20230386, -11410704, -16114594, -999085, -8142388, 5640030),
            ),
            array(
                array(10299610, 13746483, 11661824, 16234854, 7630238, 5998374, 9809887, -16694564, 15219798, -14327783),
                array(27425505, -5719081, 3055006, 10660664, 23458024, 595578, -15398605, -1173195, -18342183, 9742717),
                array(6744077, 2427284, 26042789, 2720740, -847906, 1118974, 32324614, 7406442, 12420155, 1994844),
            ),
            array(
                array(14012521, -5024720, -18384453, -9578469, -26485342, -3936439, -13033478, -10909803, 24319929, -6446333),
                array(16412690, -4507367, 10772641, 15929391, -17068788, -4658621, 10555945, -10484049, -30102368, -4739048),
                array(22397382, -7767684, -9293161, -12792868, 17166287, -9755136, -27333065, 6199366, 21880021, -12250760),
            ),
            array(
                array(-4283307, 5368523, -31117018, 8163389, -30323063, 3209128, 16557151, 8890729, 8840445, 4957760),
                array(-15447727, 709327, -6919446, -10870178, -29777922, 6522332, -21720181, 12130072, -14796503, 5005757),
                array(-2114751, -14308128, 23019042, 15765735, -25269683, 6002752, 10183197, -13239326, -16395286, -2176112),
            ),
        ),
        array(
            array(
                array(-19025756, 1632005, 13466291, -7995100, -23640451, 16573537, -32013908, -3057104, 22208662, 2000468),
                array(3065073, -1412761, -25598674, -361432, -17683065, -5703415, -8164212, 11248527, -3691214, -7414184),
                array(10379208, -6045554, 8877319, 1473647, -29291284, -12507580, 16690915, 2553332, -3132688, 16400289),
            ),
            array(
                array(15716668, 1254266, -18472690, 7446274, -8448918, 6344164, -22097271, -7285580, 26894937, 9132066),
                array(24158887, 12938817, 11085297, -8177598, -28063478, -4457083, -30576463, 64452, -6817084, -2692882),
                array(13488534, 7794716, 22236231, 5989356, 25426474, -12578208, 2350710, -3418511, -4688006, 2364226),
            ),
            array(
                array(16335052, 9132434, 25640582, 6678888, 1725628, 8517937, -11807024, -11697457, 15445875, -7798101),
                array(29004207, -7867081, 28661402, -640412, -12794003, -7943086, 31863255, -4135540, -278050, -15759279),
                array(-6122061, -14866665, -28614905, 14569919, -10857999, -3591829, 10343412, -6976290, -29828287, -10815811),
            ),
            array(
                array(27081650, 3463984, 14099042, -4517604, 1616303, -6205604, 29542636, 15372179, 17293797, 960709),
                array(20263915, 11434237, -5765435, 11236810, 13505955, -10857102, -16111345, 6493122, -19384511, 7639714),
                array(-2830798, -14839232, 25403038, -8215196, -8317012, -16173699, 18006287, -16043750, 29994677, -15808121),
            ),
            array(
                array(9769828, 5202651, -24157398, -13631392, -28051003, -11561624, -24613141, -13860782, -31184575, 709464),
                array(12286395, 13076066, -21775189, -1176622, -25003198, 4057652, -32018128, -8890874, 16102007, 13205847),
                array(13733362, 5599946, 10557076, 3195751, -5557991, 8536970, -25540170, 8525972, 10151379, 10394400),
            ),
            array(
                array(4024660, -16137551, 22436262, 12276534, -9099015, -2686099, 19698229, 11743039, -33302334, 8934414),
                array(-15879800, -4525240, -8580747, -2934061, 14634845, -698278, -9449077, 3137094, -11536886, 11721158),
                array(17555939, -5013938, 8268606, 2331751, -22738815, 9761013, 9319229, 8835153, -9205489, -1280045),
            ),
            array(
                array(-461409, -7830014, 20614118, 16688288, -7514766, -4807119, 22300304, 505429, 6108462, -6183415),
                array(-5070281, 12367917, -30663534, 3234473, 32617080, -8422642, 29880583, -13483331, -26898490, -7867459),
                array(-31975283, 5726539, 26934134, 10237677, -3173717, -605053, 24199304, 3795095, 7592688, -14992079),
            ),
            array(
                array(21594432, -14964228, 17466408, -4077222, 32537084, 2739898, 6407723, 12018833, -28256052, 4298412),
                array(-20650503, -11961496, -27236275, 570498, 3767144, -1717540, 13891942, -1569194, 13717174, 10805743),
                array(-14676630, -15644296, 15287174, 11927123, 24177847, -8175568, -796431, 14860609, -26938930, -5863836),
            ),
        ),
        array(
            array(
                array(12962541, 5311799, -10060768, 11658280, 18855286, -7954201, 13286263, -12808704, -4381056, 9882022),
                array(18512079, 11319350, -20123124, 15090309, 18818594, 5271736, -22727904, 3666879, -23967430, -3299429),
                array(-6789020, -3146043, 16192429, 13241070, 15898607, -14206114, -10084880, -6661110, -2403099, 5276065),
            ),
            array(
                array(30169808, -5317648, 26306206, -11750859, 27814964, 7069267, 7152851, 3684982, 1449224, 13082861),
                array(10342826, 3098505, 2119311, 193222, 25702612, 12233820, 23697382, 15056736, -21016438, -8202000),
                array(-33150110, 3261608, 22745853, 7948688, 19370557, -15177665, -26171976, 6482814, -10300080, -11060101),
            ),
            array(
                array(32869458, -5408545, 25609743, 15678670, -10687769, -15471071, 26112421, 2521008, -22664288, 6904815),
                array(29506923, 4457497, 3377935, -9796444, -30510046, 12935080, 1561737, 3841096, -29003639, -6657642),
                array(10340844, -6630377, -18656632, -2278430, 12621151, -13339055, 30878497, -11824370, -25584551, 5181966),
            ),
            array(
                array(25940115, -12658025, 17324188, -10307374, -8671468, 15029094, 24396252, -16450922, -2322852, -12388574),
                array(-21765684, 9916823, -1300409, 4079498, -1028346, 11909559, 1782390, 12641087, 20603771, -6561742),
                array(-18882287, -11673380, 24849422, 11501709, 13161720, -4768874, 1925523, 11914390, 4662781, 7820689),
            ),
            array(
                array(12241050, -425982, 8132691, 9393934, 32846760, -1599620, 29749456, 12172924, 16136752, 15264020),
                array(-10349955, -14680563, -8211979, 2330220, -17662549, -14545780, 10658213, 6671822, 19012087, 3772772),
                array(3753511, -3421066, 10617074, 2028709, 14841030, -6721664, 28718732, -15762884, 20527771, 12988982),
            ),
            array(
                array(-14822485, -5797269, -3707987, 12689773, -898983, -10914866, -24183046, -10564943, 3299665, -12424953),
                array(-16777703, -15253301, -9642417, 4978983, 3308785, 8755439, 6943197, 6461331, -25583147, 8991218),
                array(-17226263, 1816362, -1673288, -6086439, 31783888, -8175991, -32948145, 7417950, -30242287, 1507265),
            ),
            array(
                array(29692663, 6829891, -10498800, 4334896, 20945975, -11906496, -28887608, 8209391, 14606362, -10647073),
                array(-3481570, 8707081, 32188102, 5672294, 22096700, 1711240, -33020695, 9761487, 4170404, -2085325),
                array(-11587470, 14855945, -4127778, -1531857, -26649089, 15084046, 22186522, 16002000, -14276837, -8400798),
            ),
            array(
                array(-4811456, 13761029, -31703877, -2483919, -3312471, 7869047, -7113572, -9620092, 13240845, 10965870),
                array(-7742563, -8256762, -14768334, -13656260, -23232383, 12387166, 4498947, 14147411, 29514390, 4302863),
                array(-13413405, -12407859, 20757302, -13801832, 14785143, 8976368, -5061276, -2144373, 17846988, -13971927),
            ),
        ),
        array(
            array(
                array(-2244452, -754728, -4597030, -1066309, -6247172, 1455299, -21647728, -9214789, -5222701, 12650267),
                array(-9906797, -16070310, 21134160, 12198166, -27064575, 708126, 387813, 13770293, -19134326, 10958663),
                array(22470984, 12369526, 23446014, -5441109, -21520802, -9698723, -11772496, -11574455, -25083830, 4271862),
            ),
            array(
                array(-25169565, -10053642, -19909332, 15361595, -5984358, 2159192, 75375, -4278529, -32526221, 8469673),
                array(15854970, 4148314, -8893890, 7259002, 11666551, 13824734, -30531198, 2697372, 24154791, -9460943),
                array(15446137, -15806644, 29759747, 14019369, 30811221, -9610191, -31582008, 12840104, 24913809, 9815020),
            ),
            array(
                array(-4709286, -5614269, -31841498, -12288893, -14443537, 10799414, -9103676, 13438769, 18735128, 9466238),
                array(11933045, 9281483, 5081055, -5183824, -2628162, -4905629, -7727821, -10896103, -22728655, 16199064),
                array(14576810, 379472, -26786533, -8317236, -29426508, -10812974, -102766, 1876699, 30801119, 2164795),
            ),
            array(
                array(15995086, 3199873, 13672555, 13712240, -19378835, -4647646, -13081610, -15496269, -13492807, 1268052),
                array(-10290614, -3659039, -3286592, 10948818, 23037027, 3794475, -3470338, -12600221, -17055369, 3565904),
                array(29210088, -9419337, -5919792, -4952785, 10834811, -13327726, -16512102, -10820713, -27162222, -14030531),
            ),
            array(
                array(-13161890, 15508588, 16663704, -8156150, -28349942, 9019123, -29183421, -3769423, 2244111, -14001979),
                array(-5152875, -3800936, -9306475, -6071583, 16243069, 14684434, -25673088, -16180800, 13491506, 4641841),
                array(10813417, 643330, -19188515, -728916, 30292062, -16600078, 27548447, -7721242, 14476989, -12767431),
            ),
            array(
                array(10292079, 9984945, 6481436, 8279905, -7251514, 7032743, 27282937, -1644259, -27912810, 12651324),
                array(-31185513, -813383, 22271204, 11835308, 10201545, 15351028, 17099662, 3988035, 21721536, -3148940),
                array(10202177, -6545839, -31373232, -9574638, -32150642, -8119683, -12906320, 3852694, 13216206, 14842320),
            ),
            array(
                array(-15815640, -10601066, -6538952, -7258995, -6984659, -6581778, -31500847, 13765824, -27434397, 9900184),
                array(14465505, -13833331, -32133984, -14738873, -27443187, 12990492, 33046193, 15796406, -7051866, -8040114),
                array(30924417, -8279620, 6359016, -12816335, 16508377, 9071735, -25488601, 15413635, 9524356, -7018878),
            ),
            array(
                array(12274201, -13175547, 32627641, -1785326, 6736625, 13267305, 5237659, -5109483, 15663516, 4035784),
                array(-2951309, 8903985, 17349946, 601635, -16432815, -4612556, -13732739, -15889334, -22258478, 4659091),
                array(-16916263, -4952973, -30393711, -15158821, 20774812, 15897498, 5736189, 15026997, -2178256, -13455585),
            ),
        ),
        array(
            array(
                array(-8858980, -2219056, 28571666, -10155518, -474467, -10105698, -3801496, 278095, 23440562, -290208),
                array(10226241, -5928702, 15139956, 120818, -14867693, 5218603, 32937275, 11551483, -16571960, -7442864),
                array(17932739, -12437276, -24039557, 10749060, 11316803, 7535897, 22503767, 5561594, -3646624, 3898661),
            ),
            array(
                array(7749907, -969567, -16339731, -16464, -25018111, 15122143, -1573531, 7152530, 21831162, 1245233),
                array(26958459, -14658026, 4314586, 8346991, -5677764, 11960072, -32589295, -620035, -30402091, -16716212),
                array(-12165896, 9166947, 33491384, 13673479, 29787085, 13096535, 6280834, 14587357, -22338025, 13987525),
            ),
            array(
                array(-24349909, 7778775, 21116000, 15572597, -4833266, -5357778, -4300898, -5124639, -7469781, -2858068),
                array(9681908, -6737123, -31951644, 13591838, -6883821, 386950, 31622781, 6439245, -14581012, 4091397),
                array(-8426427, 1470727, -28109679, -1596990, 3978627, -5123623, -19622683, 12092163, 29077877, -14741988),
            ),
            array(
                array(5269168, -6859726, -13230211, -8020715, 25932563, 1763552, -5606110, -5505881, -20017847, 2357889),
                array(32264008, -15407652, -5387735, -1160093, -2091322, -3946900, 23104804, -12869908, 5727338, 189038),
                array(14609123, -8954470, -6000566, -16622781, -14577387, -7743898, -26745169, 10942115, -25888931, -14884697),
            ),
            array(
                array(20513500, 5557931, -15604613, 7829531, 26413943, -2019404, -21378968, 7471781, 13913677, -5137875),
                array(-25574376, 11967826, 29233242, 12948236, -6754465, 4713227, -8940970, 14059180, 12878652, 8511905),
                array(-25656801, 3393631, -2955415, -7075526, -2250709, 9366908, -30223418, 6812974, 5568676, -3127656),
            ),
            array(
                array(11630004, 12144454, 2116339, 13606037, 27378885, 15676917, -17408753, -13504373, -14395196, 8070818),
                array(27117696, -10007378, -31282771, -5570088, 1127282, 12772488, -29845906, 10483306, -11552749, -1028714),
                array(10637467, -5688064, 5674781, 1072708, -26343588, -6982302, -1683975, 9177853, -27493162, 15431203),
            ),
            array(
                array(20525145, 10892566, -12742472, 12779443, -29493034, 16150075, -28240519, 14943142, -15056790, -7935931),
                array(-30024462, 5626926, -551567, -9981087, 753598, 11981191, 25244767, -3239766, -3356550, 9594024),
                array(-23752644, 2636870, -5163910, -10103818, 585134, 7877383, 11345683, -6492290, 13352335, -10977084),
            ),
            array(
                array(-1931799, -5407458, 3304649, -12884869, 17015806, -4877091, -29783850, -7752482, -13215537, -319204),
                array(20239939, 6607058, 6203985, 3483793, -18386976, -779229, -20723742, 15077870, -22750759, 14523817),
                array(27406042, -6041657, 27423596, -4497394, 4996214, 10002360, -28842031, -4545494, -30172742, -4805667),
            ),
        ),
        array(
            array(
                array(11374242, 12660715, 17861383, -12540833, 10935568, 1099227, -13886076, -9091740, -27727044, 11358504),
                array(-12730809, 10311867, 1510375, 10778093, -2119455, -9145702, 32676003, 11149336, -26123651, 4985768),
                array(-19096303, 341147, -6197485, -239033, 15756973, -8796662, -983043, 13794114, -19414307, -15621255),
            ),
            array(
                array(6490081, 11940286, 25495923, -7726360, 8668373, -8751316, 3367603, 6970005, -1691065, -9004790),
                array(1656497, 13457317, 15370807, 6364910, 13605745, 8362338, -19174622, -5475723, -16796596, -5031438),
                array(-22273315, -13524424, -64685, -4334223, -18605636, -10921968, -20571065, -7007978, -99853, -10237333),
            ),
            array(
                array(17747465, 10039260, 19368299, -4050591, -20630635, -16041286, 31992683, -15857976, -29260363, -5511971),
                array(31932027, -4986141, -19612382, 16366580, 22023614, 88450, 11371999, -3744247, 4882242, -10626905),
                array(29796507, 37186, 19818052, 10115756, -11829032, 3352736, 18551198, 3272828, -5190932, -4162409),
            ),
            array(
                array(12501286, 4044383, -8612957, -13392385, -32430052, 5136599, -19230378, -3529697, 330070, -3659409),
                array(6384877, 2899513, 17807477, 7663917, -2358888, 12363165, 25366522, -8573892, -271295, 12071499),
                array(-8365515, -4042521, 25133448, -4517355, -6211027, 2265927, -32769618, 1936675, -5159697, 3829363),
            ),
            array(
                array(28425966, -5835433, -577090, -4697198, -14217555, 6870930, 7921550, -6567787, 26333140, 14267664),
                array(-11067219, 11871231, 27385719, -10559544, -4585914, -11189312, 10004786, -8709488, -21761224, 8930324),
                array(-21197785, -16396035, 25654216, -1725397, 12282012, 11008919, 1541940, 4757911, -26491501, -16408940),
            ),
            array(
                array(13537262, -7759490, -20604840, 10961927, -5922820, -13218065, -13156584, 6217254, -15943699, 13814990),
                array(-17422573, 15157790, 18705543, 29619, 24409717, -260476, 27361681, 9257833, -1956526, -1776914),
                array(-25045300, -10191966, 15366585, 15166509, -13105086, 8423556, -29171540, 12361135, -18685978, 4578290),
            ),
            array(
                array(24579768, 3711570, 1342322, -11180126, -27005135, 14124956, -22544529, 14074919, 21964432, 8235257),
                array(-6528613, -2411497, 9442966, -5925588, 12025640, -1487420, -2981514, -1669206, 13006806, 2355433),
                array(-16304899, -13605259, -6632427, -5142349, 16974359, -10911083, 27202044, 1719366, 1141648, -12796236),
            ),
            array(
                array(-12863944, -13219986, -8318266, -11018091, -6810145, -4843894, 13475066, -3133972, 32674895, 13715045),
                array(11423335, -5468059, 32344216, 8962751, 24989809, 9241752, -13265253, 16086212, -28740881, -15642093),
                array(-1409668, 12530728, -6368726, 10847387, 19531186, -14132160, -11709148, 7791794, -27245943, 4383347),
            ),
        ),
        array(
            array(
                array(-28970898, 5271447, -1266009, -9736989, -12455236, 16732599, -4862407, -4906449, 27193557, 6245191),
                array(-15193956, 5362278, -1783893, 2695834, 4960227, 12840725, 23061898, 3260492, 22510453, 8577507),
                array(-12632451, 11257346, -32692994, 13548177, -721004, 10879011, 31168030, 13952092, -29571492, -3635906),
            ),
            array(
                array(3877321, -9572739, 32416692, 5405324, -11004407, -13656635, 3759769, 11935320, 5611860, 8164018),
                array(-16275802, 14667797, 15906460, 12155291, -22111149, -9039718, 32003002, -8832289, 5773085, -8422109),
                array(-23788118, -8254300, 1950875, 8937633, 18686727, 16459170, -905725, 12376320, 31632953, 190926),
            ),
            array(
                array(-24593607, -16138885, -8423991, 13378746, 14162407, 6901328, -8288749, 4508564, -25341555, -3627528),
                array(8884438, -5884009, 6023974, 10104341, -6881569, -4941533, 18722941, -14786005, -1672488, 827625),
                array(-32720583, -16289296, -32503547, 7101210, 13354605, 2659080, -1800575, -14108036, -24878478, 1541286),
            ),
            array(
                array(2901347, -1117687, 3880376, -10059388, -17620940, -3612781, -21802117, -3567481, 20456845, -1885033),
                array(27019610, 12299467, -13658288, -1603234, -12861660, -4861471, -19540150, -5016058, 29439641, 15138866),
                array(21536104, -6626420, -32447818, -10690208, -22408077, 5175814, -5420040, -16361163, 7779328, 109896),
            ),
            array(
                array(30279744, 14648750, -8044871, 6425558, 13639621, -743509, 28698390, 12180118, 23177719, -554075),
                array(26572847, 3405927, -31701700, 12890905, -19265668, 5335866, -6493768, 2378492, 4439158, -13279347),
                array(-22716706, 3489070, -9225266, -332753, 18875722, -1140095, 14819434, -12731527, -17717757, -5461437),
            ),
            array(
                array(-5056483, 16566551, 15953661, 3767752, -10436499, 15627060, -820954, 2177225, 8550082, -15114165),
                array(-18473302, 16596775, -381660, 15663611, 22860960, 15585581, -27844109, -3582739, -23260460, -8428588),
                array(-32480551, 15707275, -8205912, -5652081, 29464558, 2713815, -22725137, 15860482, -21902570, 1494193),
            ),
            array(
                array(-19562091, -14087393, -25583872, -9299552, 13127842, 759709, 21923482, 16529112, 8742704, 12967017),
                array(-28464899, 1553205, 32536856, -10473729, -24691605, -406174, -8914625, -2933896, -29903758, 15553883),
                array(21877909, 3230008, 9881174, 10539357, -4797115, 2841332, 11543572, 14513274, 19375923, -12647961),
            ),
            array(
                array(8832269, -14495485, 13253511, 5137575, 5037871, 4078777, 24880818, -6222716, 2862653, 9455043),
                array(29306751, 5123106, 20245049, -14149889, 9592566, 8447059, -2077124, -2990080, 15511449, 4789663),
                array(-20679756, 7004547, 8824831, -9434977, -4045704, -3750736, -5754762, 108893, 23513200, 16652362),
            ),
        ),
        array(
            array(
                array(-33256173, 4144782, -4476029, -6579123, 10770039, -7155542, -6650416, -12936300, -18319198, 10212860),
                array(2756081, 8598110, 7383731, -6859892, 22312759, -1105012, 21179801, 2600940, -9988298, -12506466),
                array(-24645692, 13317462, -30449259, -15653928, 21365574, -10869657, 11344424, 864440, -2499677, -16710063),
            ),
            array(
                array(-26432803, 6148329, -17184412, -14474154, 18782929, -275997, -22561534, 211300, 2719757, 4940997),
                array(-1323882, 3911313, -6948744, 14759765, -30027150, 7851207, 21690126, 8518463, 26699843, 5276295),
                array(-13149873, -6429067, 9396249, 365013, 24703301, -10488939, 1321586, 149635, -15452774, 7159369),
            ),
            array(
                array(9987780, -3404759, 17507962, 9505530, 9731535, -2165514, 22356009, 8312176, 22477218, -8403385),
                array(18155857, -16504990, 19744716, 9006923, 15154154, -10538976, 24256460, -4864995, -22548173, 9334109),
                array(2986088, -4911893, 10776628, -3473844, 10620590, -7083203, -21413845, 14253545, -22587149, 536906),
            ),
            array(
                array(4377756, 8115836, 24567078, 15495314, 11625074, 13064599, 7390551, 10589625, 10838060, -15420424),
                array(-19342404, 867880, 9277171, -3218459, -14431572, -1986443, 19295826, -15796950, 6378260, 699185),
                array(7895026, 4057113, -7081772, -13077756, -17886831, -323126, -716039, 15693155, -5045064, -13373962),
            ),
            array(
                array(-7737563, -5869402, -14566319, -7406919, 11385654, 13201616, 31730678, -10962840, -3918636, -9669325),
                array(10188286, -15770834, -7336361, 13427543, 22223443, 14896287, 30743455, 7116568, -21786507, 5427593),
                array(696102, 13206899, 27047647, -10632082, 15285305, -9853179, 10798490, -4578720, 19236243, 12477404),
            ),
            array(
                array(-11229439, 11243796, -17054270, -8040865, -788228, -8167967, -3897669, 11180504, -23169516, 7733644),
                array(17800790, -14036179, -27000429, -11766671, 23887827, 3149671, 23466177, -10538171, 10322027, 15313801),
                array(26246234, 11968874, 32263343, -5468728, 6830755, -13323031, -15794704, -101982, -24449242, 10890804),
            ),
            array(
                array(-31365647, 10271363, -12660625, -6267268, 16690207, -13062544, -14982212, 16484931, 25180797, -5334884),
                array(-586574, 10376444, -32586414, -11286356, 19801893, 10997610, 2276632, 9482883, 316878, 13820577),
                array(-9882808, -4510367, -2115506, 16457136, -11100081, 11674996, 30756178, -7515054, 30696930, -3712849),
            ),
            array(
                array(32988917, -9603412, 12499366, 7910787, -10617257, -11931514, -7342816, -9985397, -32349517, 7392473),
                array(-8855661, 15927861, 9866406, -3649411, -2396914, -16655781, -30409476, -9134995, 25112947, -2926644),
                array(-2504044, -436966, 25621774, -5678772, 15085042, -5479877, -24884878, -13526194, 5537438, -13914319),
            ),
        ),
        array(
            array(
                array(-11225584, 2320285, -9584280, 10149187, -33444663, 5808648, -14876251, -1729667, 31234590, 6090599),
                array(-9633316, 116426, 26083934, 2897444, -6364437, -2688086, 609721, 15878753, -6970405, -9034768),
                array(-27757857, 247744, -15194774, -9002551, 23288161, -10011936, -23869595, 6503646, 20650474, 1804084),
            ),
            array(
                array(-27589786, 15456424, 8972517, 8469608, 15640622, 4439847, 3121995, -10329713, 27842616, -202328),
                array(-15306973, 2839644, 22530074, 10026331, 4602058, 5048462, 28248656, 5031932, -11375082, 12714369),
                array(20807691, -7270825, 29286141, 11421711, -27876523, -13868230, -21227475, 1035546, -19733229, 12796920),
            ),
            array(
                array(12076899, -14301286, -8785001, -11848922, -25012791, 16400684, -17591495, -12899438, 3480665, -15182815),
                array(-32361549, 5457597, 28548107, 7833186, 7303070, -11953545, -24363064, -15921875, -33374054, 2771025),
                array(-21389266, 421932, 26597266, 6860826, 22486084, -6737172, -17137485, -4210226, -24552282, 15673397),
            ),
            array(
                array(-20184622, 2338216, 19788685, -9620956, -4001265, -8740893, -20271184, 4733254, 3727144, -12934448),
                array(6120119, 814863, -11794402, -622716, 6812205, -15747771, 2019594, 7975683, 31123697, -10958981),
                array(30069250, -11435332, 30434654, 2958439, 18399564, -976289, 12296869, 9204260, -16432438, 9648165),
            ),
            array(
                array(32705432, -1550977, 30705658, 7451065, -11805606, 9631813, 3305266, 5248604, -26008332, -11377501),
                array(17219865, 2375039, -31570947, -5575615, -19459679, 9219903, 294711, 15298639, 2662509, -16297073),
                array(-1172927, -7558695, -4366770, -4287744, -21346413, -8434326, 32087529, -1222777, 32247248, -14389861),
            ),
            array(
                array(14312628, 1221556, 17395390, -8700143, -4945741, -8684635, -28197744, -9637817, -16027623, -13378845),
                array(-1428825, -9678990, -9235681, 6549687, -7383069, -468664, 23046502, 9803137, 17597934, 2346211),
                array(18510800, 15337574, 26171504, 981392, -22241552, 7827556, -23491134, -11323352, 3059833, -11782870),
            ),
            array(
                array(10141598, 6082907, 17829293, -1947643, 9830092, 13613136, -25556636, -5544586, -33502212, 3592096),
                array(33114168, -15889352, -26525686, -13343397, 33076705, 8716171, 1151462, 1521897, -982665, -6837803),
                array(-32939165, -4255815, 23947181, -324178, -33072974, -12305637, -16637686, 3891704, 26353178, 693168),
            ),
            array(
                array(30374239, 1595580, -16884039, 13186931, 4600344, 406904, 9585294, -400668, 31375464, 14369965),
                array(-14370654, -7772529, 1510301, 6434173, -18784789, -6262728, 32732230, -13108839, 17901441, 16011505),
                array(18171223, -11934626, -12500402, 15197122, -11038147, -15230035, -19172240, -16046376, 8764035, 12309598),
            ),
        ),
        array(
            array(
                array(5975908, -5243188, -19459362, -9681747, -11541277, 14015782, -23665757, 1228319, 17544096, -10593782),
                array(5811932, -1715293, 3442887, -2269310, -18367348, -8359541, -18044043, -15410127, -5565381, 12348900),
                array(-31399660, 11407555, 25755363, 6891399, -3256938, 14872274, -24849353, 8141295, -10632534, -585479),
            ),
            array(
                array(-12675304, 694026, -5076145, 13300344, 14015258, -14451394, -9698672, -11329050, 30944593, 1130208),
                array(8247766, -6710942, -26562381, -7709309, -14401939, -14648910, 4652152, 2488540, 23550156, -271232),
                array(17294316, -3788438, 7026748, 15626851, 22990044, 113481, 2267737, -5908146, -408818, -137719),
            ),
            array(
                array(16091085, -16253926, 18599252, 7340678, 2137637, -1221657, -3364161, 14550936, 3260525, -7166271),
                array(-4910104, -13332887, 18550887, 10864893, -16459325, -7291596, -23028869, -13204905, -12748722, 2701326),
                array(-8574695, 16099415, 4629974, -16340524, -20786213, -6005432, -10018363, 9276971, 11329923, 1862132),
            ),
            array(
                array(14763076, -15903608, -30918270, 3689867, 3511892, 10313526, -21951088, 12219231, -9037963, -940300),
                array(8894987, -3446094, 6150753, 3013931, 301220, 15693451, -31981216, -2909717, -15438168, 11595570),
                array(15214962, 3537601, -26238722, -14058872, 4418657, -15230761, 13947276, 10730794, -13489462, -4363670),
            ),
            array(
                array(-2538306, 7682793, 32759013, 263109, -29984731, -7955452, -22332124, -10188635, 977108, 699994),
                array(-12466472, 4195084, -9211532, 550904, -15565337, 12917920, 19118110, -439841, -30534533, -14337913),
                array(31788461, -14507657, 4799989, 7372237, 8808585, -14747943, 9408237, -10051775, 12493932, -5409317),
            ),
            array(
                array(-25680606, 5260744, -19235809, -6284470, -3695942, 16566087, 27218280, 2607121, 29375955, 6024730),
                array(842132, -2794693, -4763381, -8722815, 26332018, -12405641, 11831880, 6985184, -9940361, 2854096),
                array(-4847262, -7969331, 2516242, -5847713, 9695691, -7221186, 16512645, 960770, 12121869, 16648078),
            ),
            array(
                array(-15218652, 14667096, -13336229, 2013717, 30598287, -464137, -31504922, -7882064, 20237806, 2838411),
                array(-19288047, 4453152, 15298546, -16178388, 22115043, -15972604, 12544294, -13470457, 1068881, -12499905),
                array(-9558883, -16518835, 33238498, 13506958, 30505848, -1114596, -8486907, -2630053, 12521378, 4845654),
            ),
            array(
                array(-28198521, 10744108, -2958380, 10199664, 7759311, -13088600, 3409348, -873400, -6482306, -12885870),
                array(-23561822, 6230156, -20382013, 10655314, -24040585, -11621172, 10477734, -1240216, -3113227, 13974498),
                array(12966261, 15550616, -32038948, -1615346, 21025980, -629444, 5642325, 7188737, 18895762, 12629579),
            ),
        ),
        array(
            array(
                array(14741879, -14946887, 22177208, -11721237, 1279741, 8058600, 11758140, 789443, 32195181, 3895677),
                array(10758205, 15755439, -4509950, 9243698, -4879422, 6879879, -2204575, -3566119, -8982069, 4429647),
                array(-2453894, 15725973, -20436342, -10410672, -5803908, -11040220, -7135870, -11642895, 18047436, -15281743),
            ),
            array(
                array(-25173001, -11307165, 29759956, 11776784, -22262383, -15820455, 10993114, -12850837, -17620701, -9408468),
                array(21987233, 700364, -24505048, 14972008, -7774265, -5718395, 32155026, 2581431, -29958985, 8773375),
                array(-25568350, 454463, -13211935, 16126715, 25240068, 8594567, 20656846, 12017935, -7874389, -13920155),
            ),
            array(
                array(6028182, 6263078, -31011806, -11301710, -818919, 2461772, -31841174, -5468042, -1721788, -2776725),
                array(-12278994, 16624277, 987579, -5922598, 32908203, 1248608, 7719845, -4166698, 28408820, 6816612),
                array(-10358094, -8237829, 19549651, -12169222, 22082623, 16147817, 20613181, 13982702, -10339570, 5067943),
            ),
            array(
                array(-30505967, -3821767, 12074681, 13582412, -19877972, 2443951, -19719286, 12746132, 5331210, -10105944),
                array(30528811, 3601899, -1957090, 4619785, -27361822, -15436388, 24180793, -12570394, 27679908, -1648928),
                array(9402404, -13957065, 32834043, 10838634, -26580150, -13237195, 26653274, -8685565, 22611444, -12715406),
            ),
            array(
                array(22190590, 1118029, 22736441, 15130463, -30460692, -5991321, 19189625, -4648942, 4854859, 6622139),
                array(-8310738, -2953450, -8262579, -3388049, -10401731, -271929, 13424426, -3567227, 26404409, 13001963),
                array(-31241838, -15415700, -2994250, 8939346, 11562230, -12840670, -26064365, -11621720, -15405155, 11020693),
            ),
            array(
                array(1866042, -7949489, -7898649, -10301010, 12483315, 13477547, 3175636, -12424163, 28761762, 1406734),
                array(-448555, -1777666, 13018551, 3194501, -9580420, -11161737, 24760585, -4347088, 25577411, -13378680),
                array(-24290378, 4759345, -690653, -1852816, 2066747, 10693769, -29595790, 9884936, -9368926, 4745410),
            ),
            array(
                array(-9141284, 6049714, -19531061, -4341411, -31260798, 9944276, -15462008, -11311852, 10931924, -11931931),
                array(-16561513, 14112680, -8012645, 4817318, -8040464, -11414606, -22853429, 10856641, -20470770, 13434654),
                array(22759489, -10073434, -16766264, -1871422, 13637442, -10168091, 1765144, -12654326, 28445307, -5364710),
            ),
            array(
                array(29875063, 12493613, 2795536, -3786330, 1710620, 15181182, -10195717, -8788675, 9074234, 1167180),
                array(-26205683, 11014233, -9842651, -2635485, -26908120, 7532294, -18716888, -9535498, 3843903, 9367684),
                array(-10969595, -6403711, 9591134, 9582310, 11349256, 108879, 16235123, 8601684, -139197, 4242895),
            ),
        ),
        array(
            array(
                array(22092954, -13191123, -2042793, -11968512, 32186753, -11517388, -6574341, 2470660, -27417366, 16625501),
                array(-11057722, 3042016, 13770083, -9257922, 584236, -544855, -7770857, 2602725, -27351616, 14247413),
                array(6314175, -10264892, -32772502, 15957557, -10157730, 168750, -8618807, 14290061, 27108877, -1180880),
            ),
            array(
                array(-8586597, -7170966, 13241782, 10960156, -32991015, -13794596, 33547976, -11058889, -27148451, 981874),
                array(22833440, 9293594, -32649448, -13618667, -9136966, 14756819, -22928859, -13970780, -10479804, -16197962),
                array(-7768587, 3326786, -28111797, 10783824, 19178761, 14905060, 22680049, 13906969, -15933690, 3797899),
            ),
            array(
                array(21721356, -4212746, -12206123, 9310182, -3882239, -13653110, 23740224, -2709232, 20491983, -8042152),
                array(9209270, -15135055, -13256557, -6167798, -731016, 15289673, 25947805, 15286587, 30997318, -6703063),
                array(7392032, 16618386, 23946583, -8039892, -13265164, -1533858, -14197445, -2321576, 17649998, -250080),
            ),
            array(
                array(-9301088, -14193827, 30609526, -3049543, -25175069, -1283752, -15241566, -9525724, -2233253, 7662146),
                array(-17558673, 1763594, -33114336, 15908610, -30040870, -12174295, 7335080, -8472199, -3174674, 3440183),
                array(-19889700, -5977008, -24111293, -9688870, 10799743, -16571957, 40450, -4431835, 4862400, 1133),
            ),
            array(
                array(-32856209, -7873957, -5422389, 14860950, -16319031, 7956142, 7258061, 311861, -30594991, -7379421),
                array(-3773428, -1565936, 28985340, 7499440, 24445838, 9325937, 29727763, 16527196, 18278453, 15405622),
                array(-4381906, 8508652, -19898366, -3674424, -5984453, 15149970, -13313598, 843523, -21875062, 13626197),
            ),
            array(
                array(2281448, -13487055, -10915418, -2609910, 1879358, 16164207, -10783882, 3953792, 13340839, 15928663),
                array(31727126, -7179855, -18437503, -8283652, 2875793, -16390330, -25269894, -7014826, -23452306, 5964753),
                array(4100420, -5959452, -17179337, 6017714, -18705837, 12227141, -26684835, 11344144, 2538215, -7570755),
            ),
            array(
                array(-9433605, 6123113, 11159803, -2156608, 30016280, 14966241, -20474983, 1485421, -629256, -15958862),
                array(-26804558, 4260919, 11851389, 9658551, -32017107, 16367492, -20205425, -13191288, 11659922, -11115118),
                array(26180396, 10015009, -30844224, -8581293, 5418197, 9480663, 2231568, -10170080, 33100372, -1306171),
            ),
            array(
                array(15121113, -5201871, -10389905, 15427821, -27509937, -15992507, 21670947, 4486675, -5931810, -14466380),
                array(16166486, -9483733, -11104130, 6023908, -31926798, -1364923, 2340060, -16254968, -10735770, -10039824),
                array(28042865, -3557089, -12126526, 12259706, -3717498, -6945899, 6766453, -8689599, 18036436, 5803270),
            ),
        ),
        array(
            array(
                array(-817581, 6763912, 11803561, 1585585, 10958447, -2671165, 23855391, 4598332, -6159431, -14117438),
                array(-31031306, -14256194, 17332029, -2383520, 31312682, -5967183, 696309, 50292, -20095739, 11763584),
                array(-594563, -2514283, -32234153, 12643980, 12650761, 14811489, 665117, -12613632, -19773211, -10713562),
            ),
            array(
                array(30464590, -11262872, -4127476, -12734478, 19835327, -7105613, -24396175, 2075773, -17020157, 992471),
                array(18357185, -6994433, 7766382, 16342475, -29324918, 411174, 14578841, 8080033, -11574335, -10601610),
                array(19598397, 10334610, 12555054, 2555664, 18821899, -10339780, 21873263, 16014234, 26224780, 16452269),
            ),
            array(
                array(-30223925, 5145196, 5944548, 16385966, 3976735, 2009897, -11377804, -7618186, -20533829, 3698650),
                array(14187449, 3448569, -10636236, -10810935, -22663880, -3433596, 7268410, -10890444, 27394301, 12015369),
                array(19695761, 16087646, 28032085, 12999827, 6817792, 11427614, 20244189, -1312777, -13259127, -3402461),
            ),
            array(
                array(30860103, 12735208, -1888245, -4699734, -16974906, 2256940, -8166013, 12298312, -8550524, -10393462),
                array(-5719826, -11245325, -1910649, 15569035, 26642876, -7587760, -5789354, -15118654, -4976164, 12651793),
                array(-2848395, 9953421, 11531313, -5282879, 26895123, -12697089, -13118820, -16517902, 9768698, -2533218),
            ),
            array(
                array(-24719459, 1894651, -287698, -4704085, 15348719, -8156530, 32767513, 12765450, 4940095, 10678226),
                array(18860224, 15980149, -18987240, -1562570, -26233012, -11071856, -7843882, 13944024, -24372348, 16582019),
                array(-15504260, 4970268, -29893044, 4175593, -20993212, -2199756, -11704054, 15444560, -11003761, 7989037),
            ),
            array(
                array(31490452, 5568061, -2412803, 2182383, -32336847, 4531686, -32078269, 6200206, -19686113, -14800171),
                array(-17308668, -15879940, -31522777, -2831, -32887382, 16375549, 8680158, -16371713, 28550068, -6857132),
                array(-28126887, -5688091, 16837845, -1820458, -6850681, 12700016, -30039981, 4364038, 1155602, 5988841),
            ),
            array(
                array(21890435, -13272907, -12624011, 12154349, -7831873, 15300496, 23148983, -4470481, 24618407, 8283181),
                array(-33136107, -10512751, 9975416, 6841041, -31559793, 16356536, 3070187, -7025928, 1466169, 10740210),
                array(-1509399, -15488185, -13503385, -10655916, 32799044, 909394, -13938903, -5779719, -32164649, -15327040),
            ),
            array(
                array(3960823, -14267803, -28026090, -15918051, -19404858, 13146868, 15567327, 951507, -3260321, -573935),
                array(24740841, 5052253, -30094131, 8961361, 25877428, 6165135, -24368180, 14397372, -7380369, -6144105),
                array(-28888365, 3510803, -28103278, -1158478, -11238128, -10631454, -15441463, -14453128, -1625486, -6494814),
            ),
        ),
        array(
            array(
                array(793299, -9230478, 8836302, -6235707, -27360908, -2369593, 33152843, -4885251, -9906200, -621852),
                array(5666233, 525582, 20782575, -8038419, -24538499, 14657740, 16099374, 1468826, -6171428, -15186581),
                array(-4859255, -3779343, -2917758, -6748019, 7778750, 11688288, -30404353, -9871238, -1558923, -9863646),
            ),
            array(
                array(10896332, -7719704, 824275, 472601, -19460308, 3009587, 25248958, 14783338, -30581476, -15757844),
                array(10566929, 12612572, -31944212, 11118703, -12633376, 12362879, 21752402, 8822496, 24003793, 14264025),
                array(27713862, -7355973, -11008240, 9227530, 27050101, 2504721, 23886875, -13117525, 13958495, -5732453),
            ),
            array(
                array(-23481610, 4867226, -27247128, 3900521, 29838369, -8212291, -31889399, -10041781, 7340521, -15410068),
                array(4646514, -8011124, -22766023, -11532654, 23184553, 8566613, 31366726, -1381061, -15066784, -10375192),
                array(-17270517, 12723032, -16993061, 14878794, 21619651, -6197576, 27584817, 3093888, -8843694, 3849921),
            ),
            array(
                array(-9064912, 2103172, 25561640, -15125738, -5239824, 9582958, 32477045, -9017955, 5002294, -15550259),
                array(-12057553, -11177906, 21115585, -13365155, 8808712, -12030708, 16489530, 13378448, -25845716, 12741426),
                array(-5946367, 10645103, -30911586, 15390284, -3286982, -7118677, 24306472, 15852464, 28834118, -7646072),
            ),
            array(
                array(-17335748, -9107057, -24531279, 9434953, -8472084, -583362, -13090771, 455841, 20461858, 5491305),
                array(13669248, -16095482, -12481974, -10203039, -14569770, -11893198, -24995986, 11293807, -28588204, -9421832),
                array(28497928, 6272777, -33022994, 14470570, 8906179, -1225630, 18504674, -14165166, 29867745, -8795943),
            ),
            array(
                array(-16207023, 13517196, -27799630, -13697798, 24009064, -6373891, -6367600, -13175392, 22853429, -4012011),
                array(24191378, 16712145, -13931797, 15217831, 14542237, 1646131, 18603514, -11037887, 12876623, -2112447),
                array(17902668, 4518229, -411702, -2829247, 26878217, 5258055, -12860753, 608397, 16031844, 3723494),
            ),
            array(
                array(-28632773, 12763728, -20446446, 7577504, 33001348, -13017745, 17558842, -7872890, 23896954, -4314245),
                array(-20005381, -12011952, 31520464, 605201, 2543521, 5991821, -2945064, 7229064, -9919646, -8826859),
                array(28816045, 298879, -28165016, -15920938, 19000928, -1665890, -12680833, -2949325, -18051778, -2082915),
            ),
            array(
                array(16000882, -344896, 3493092, -11447198, -29504595, -13159789, 12577740, 16041268, -19715240, 7847707),
                array(10151868, 10572098, 27312476, 7922682, 14825339, 4723128, -32855931, -6519018, -10020567, 3852848),
                array(-11430470, 15697596, -21121557, -4420647, 5386314, 15063598, 16514493, -15932110, 29330899, -15076224),
            ),
        ),
        array(
            array(
                array(-25499735, -4378794, -15222908, -6901211, 16615731, 2051784, 3303702, 15490, -27548796, 12314391),
                array(15683520, -6003043, 18109120, -9980648, 15337968, -5997823, -16717435, 15921866, 16103996, -3731215),
                array(-23169824, -10781249, 13588192, -1628807, -3798557, -1074929, -19273607, 5402699, -29815713, -9841101),
            ),
            array(
                array(23190676, 2384583, -32714340, 3462154, -29903655, -1529132, -11266856, 8911517, -25205859, 2739713),
                array(21374101, -3554250, -33524649, 9874411, 15377179, 11831242, -33529904, 6134907, 4931255, 11987849),
                array(-7732, -2978858, -16223486, 7277597, 105524, -322051, -31480539, 13861388, -30076310, 10117930),
            ),
            array(
                array(-29501170, -10744872, -26163768, 13051539, -25625564, 5089643, -6325503, 6704079, 12890019, 15728940),
                array(-21972360, -11771379, -951059, -4418840, 14704840, 2695116, 903376, -10428139, 12885167, 8311031),
                array(-17516482, 5352194, 10384213, -13811658, 7506451, 13453191, 26423267, 4384730, 1888765, -5435404),
            ),
            array(
                array(-25817338, -3107312, -13494599, -3182506, 30896459, -13921729, -32251644, -12707869, -19464434, -3340243),
                array(-23607977, -2665774, -526091, 4651136, 5765089, 4618330, 6092245, 14845197, 17151279, -9854116),
                array(-24830458, -12733720, -15165978, 10367250, -29530908, -265356, 22825805, -7087279, -16866484, 16176525),
            ),
            array(
                array(-23583256, 6564961, 20063689, 3798228, -4740178, 7359225, 2006182, -10363426, -28746253, -10197509),
                array(-10626600, -4486402, -13320562, -5125317, 3432136, -6393229, 23632037, -1940610, 32808310, 1099883),
                array(15030977, 5768825, -27451236, -2887299, -6427378, -15361371, -15277896, -6809350, 2051441, -15225865),
            ),
            array(
                array(-3362323, -7239372, 7517890, 9824992, 23555850, 295369, 5148398, -14154188, -22686354, 16633660),
                array(4577086, -16752288, 13249841, -15304328, 19958763, -14537274, 18559670, -10759549, 8402478, -9864273),
                array(-28406330, -1051581, -26790155, -907698, -17212414, -11030789, 9453451, -14980072, 17983010, 9967138),
            ),
            array(
                array(-25762494, 6524722, 26585488, 9969270, 24709298, 1220360, -1677990, 7806337, 17507396, 3651560),
                array(-10420457, -4118111, 14584639, 15971087, -15768321, 8861010, 26556809, -5574557, -18553322, -11357135),
                array(2839101, 14284142, 4029895, 3472686, 14402957, 12689363, -26642121, 8459447, -5605463, -7621941),
            ),
            array(
                array(-4839289, -3535444, 9744961, 2871048, 25113978, 3187018, -25110813, -849066, 17258084, -7977739),
                array(18164541, -10595176, -17154882, -1542417, 19237078, -9745295, 23357533, -15217008, 26908270, 12150756),
                array(-30264870, -7647865, 5112249, -7036672, -1499807, -6974257, 43168, -5537701, -32302074, 16215819),
            ),
        ),
        array(
            array(
                array(-6898905, 9824394, -12304779, -4401089, -31397141, -6276835, 32574489, 12532905, -7503072, -8675347),
                array(-27343522, -16515468, -27151524, -10722951, 946346, 16291093, 254968, 7168080, 21676107, -1943028),
                array(21260961, -8424752, -16831886, -11920822, -23677961, 3968121, -3651949, -6215466, -3556191, -7913075),
            ),
            array(
                array(16544754, 13250366, -16804428, 15546242, -4583003, 12757258, -2462308, -8680336, -18907032, -9662799),
                array(-2415239, -15577728, 18312303, 4964443, -15272530, -12653564, 26820651, 16690659, 25459437, -4564609),
                array(-25144690, 11425020, 28423002, -11020557, -6144921, -15826224, 9142795, -2391602, -6432418, -1644817),
            ),
            array(
                array(-23104652, 6253476, 16964147, -3768872, -25113972, -12296437, -27457225, -16344658, 6335692, 7249989),
                array(-30333227, 13979675, 7503222, -12368314, -11956721, -4621693, -30272269, 2682242, 25993170, -12478523),
                array(4364628, 5930691, 32304656, -10044554, -8054781, 15091131, 22857016, -10598955, 31820368, 15075278),
            ),
            array(
                array(31879134, -8918693, 17258761, 90626, -8041836, -4917709, 24162788, -9650886, -17970238, 12833045),
                array(19073683, 14851414, -24403169, -11860168, 7625278, 11091125, -19619190, 2074449, -9413939, 14905377),
                array(24483667, -11935567, -2518866, -11547418, -1553130, 15355506, -25282080, 9253129, 27628530, -7555480),
            ),
            array(
                array(17597607, 8340603, 19355617, 552187, 26198470, -3176583, 4593324, -9157582, -14110875, 15297016),
                array(510886, 14337390, -31785257, 16638632, 6328095, 2713355, -20217417, -11864220, 8683221, 2921426),
                array(18606791, 11874196, 27155355, -5281482, -24031742, 6265446, -25178240, -1278924, 4674690, 13890525),
            ),
            array(
                array(13609624, 13069022, -27372361, -13055908, 24360586, 9592974, 14977157, 9835105, 4389687, 288396),
                array(9922506, -519394, 13613107, 5883594, -18758345, -434263, -12304062, 8317628, 23388070, 16052080),
                array(12720016, 11937594, -31970060, -5028689, 26900120, 8561328, -20155687, -11632979, -14754271, -10812892),
            ),
            array(
                array(15961858, 14150409, 26716931, -665832, -22794328, 13603569, 11829573, 7467844, -28822128, 929275),
                array(11038231, -11582396, -27310482, -7316562, -10498527, -16307831, -23479533, -9371869, -21393143, 2465074),
                array(20017163, -4323226, 27915242, 1529148, 12396362, 15675764, 13817261, -9658066, 2463391, -4622140),
            ),
            array(
                array(-16358878, -12663911, -12065183, 4996454, -1256422, 1073572, 9583558, 12851107, 4003896, 12673717),
                array(-1731589, -15155870, -3262930, 16143082, 19294135, 13385325, 14741514, -9103726, 7903886, 2348101),
                array(24536016, -16515207, 12715592, -3862155, 1511293, 10047386, -3842346, -7129159, -28377538, 10048127),
            ),
        ),
        array(
            array(
                array(-12622226, -6204820, 30718825, 2591312, -10617028, 12192840, 18873298, -7297090, -32297756, 15221632),
                array(-26478122, -11103864, 11546244, -1852483, 9180880, 7656409, -21343950, 2095755, 29769758, 6593415),
                array(-31994208, -2907461, 4176912, 3264766, 12538965, -868111, 26312345, -6118678, 30958054, 8292160),
            ),
            array(
                array(31429822, -13959116, 29173532, 15632448, 12174511, -2760094, 32808831, 3977186, 26143136, -3148876),
                array(22648901, 1402143, -22799984, 13746059, 7936347, 365344, -8668633, -1674433, -3758243, -2304625),
                array(-15491917, 8012313, -2514730, -12702462, -23965846, -10254029, -1612713, -1535569, -16664475, 8194478),
            ),
            array(
                array(27338066, -7507420, -7414224, 10140405, -19026427, -6589889, 27277191, 8855376, 28572286, 3005164),
                array(26287124, 4821776, 25476601, -4145903, -3764513, -15788984, -18008582, 1182479, -26094821, -13079595),
                array(-7171154, 3178080, 23970071, 6201893, -17195577, -4489192, -21876275, -13982627, 32208683, -1198248),
            ),
            array(
                array(-16657702, 2817643, -10286362, 14811298, 6024667, 13349505, -27315504, -10497842, -27672585, -11539858),
                array(15941029, -9405932, -21367050, 8062055, 31876073, -238629, -15278393, -1444429, 15397331, -4130193),
                array(8934485, -13485467, -23286397, -13423241, -32446090, 14047986, 31170398, -1441021, -27505566, 15087184),
            ),
            array(
                array(-18357243, -2156491, 24524913, -16677868, 15520427, -6360776, -15502406, 11461896, 16788528, -5868942),
                array(-1947386, 16013773, 21750665, 3714552, -17401782, -16055433, -3770287, -10323320, 31322514, -11615635),
                array(21426655, -5650218, -13648287, -5347537, -28812189, -4920970, -18275391, -14621414, 13040862, -12112948),
            ),
            array(
                array(11293895, 12478086, -27136401, 15083750, -29307421, 14748872, 14555558, -13417103, 1613711, 4896935),
                array(-25894883, 15323294, -8489791, -8057900, 25967126, -13425460, 2825960, -4897045, -23971776, -11267415),
                array(-15924766, -5229880, -17443532, 6410664, 3622847, 10243618, 20615400, 12405433, -23753030, -8436416),
            ),
            array(
                array(-7091295, 12556208, -20191352, 9025187, -17072479, 4333801, 4378436, 2432030, 23097949, -566018),
                array(4565804, -16025654, 20084412, -7842817, 1724999, 189254, 24767264, 10103221, -18512313, 2424778),
                array(366633, -11976806, 8173090, -6890119, 30788634, 5745705, -7168678, 1344109, -3642553, 12412659),
            ),
            array(
                array(-24001791, 7690286, 14929416, -168257, -32210835, -13412986, 24162697, -15326504, -3141501, 11179385),
                array(18289522, -14724954, 8056945, 16430056, -21729724, 7842514, -6001441, -1486897, -18684645, -11443503),
                array(476239, 6601091, -6152790, -9723375, 17503545, -4863900, 27672959, 13403813, 11052904, 5219329),
            ),
        ),
        array(
            array(
                array(20678546, -8375738, -32671898, 8849123, -5009758, 14574752, 31186971, -3973730, 9014762, -8579056),
                array(-13644050, -10350239, -15962508, 5075808, -1514661, -11534600, -33102500, 9160280, 8473550, -3256838),
                array(24900749, 14435722, 17209120, -15292541, -22592275, 9878983, -7689309, -16335821, -24568481, 11788948),
            ),
            array(
                array(-3118155, -11395194, -13802089, 14797441, 9652448, -6845904, -20037437, 10410733, -24568470, -1458691),
                array(-15659161, 16736706, -22467150, 10215878, -9097177, 7563911, 11871841, -12505194, -18513325, 8464118),
                array(-23400612, 8348507, -14585951, -861714, -3950205, -6373419, 14325289, 8628612, 33313881, -8370517),
            ),
            array(
                array(-20186973, -4967935, 22367356, 5271547, -1097117, -4788838, -24805667, -10236854, -8940735, -5818269),
                array(-6948785, -1795212, -32625683, -16021179, 32635414, -7374245, 15989197, -12838188, 28358192, -4253904),
                array(-23561781, -2799059, -32351682, -1661963, -9147719, 10429267, -16637684, 4072016, -5351664, 5596589),
            ),
            array(
                array(-28236598, -3390048, 12312896, 6213178, 3117142, 16078565, 29266239, 2557221, 1768301, 15373193),
                array(-7243358, -3246960, -4593467, -7553353, -127927, -912245, -1090902, -4504991, -24660491, 3442910),
                array(-30210571, 5124043, 14181784, 8197961, 18964734, -11939093, 22597931, 7176455, -18585478, 13365930),
            ),
            array(
                array(-7877390, -1499958, 8324673, 4690079, 6261860, 890446, 24538107, -8570186, -9689599, -3031667),
                array(25008904, -10771599, -4305031, -9638010, 16265036, 15721635, 683793, -11823784, 15723479, -15163481),
                array(-9660625, 12374379, -27006999, -7026148, -7724114, -12314514, 11879682, 5400171, 519526, -1235876),
            ),
            array(
                array(22258397, -16332233, -7869817, 14613016, -22520255, -2950923, -20353881, 7315967, 16648397, 7605640),
                array(-8081308, -8464597, -8223311, 9719710, 19259459, -15348212, 23994942, -5281555, -9468848, 4763278),
                array(-21699244, 9220969, -15730624, 1084137, -25476107, -2852390, 31088447, -7764523, -11356529, 728112),
            ),
            array(
                array(26047220, -11751471, -6900323, -16521798, 24092068, 9158119, -4273545, -12555558, -29365436, -5498272),
                array(17510331, -322857, 5854289, 8403524, 17133918, -3112612, -28111007, 12327945, 10750447, 10014012),
                array(-10312768, 3936952, 9156313, -8897683, 16498692, -994647, -27481051, -666732, 3424691, 7540221),
            ),
            array(
                array(30322361, -6964110, 11361005, -4143317, 7433304, 4989748, -7071422, -16317219, -9244265, 15258046),
                array(13054562, -2779497, 19155474, 469045, -12482797, 4566042, 5631406, 2711395, 1062915, -5136345),
                array(-19240248, -11254599, -29509029, -7499965, -5835763, 13005411, -6066489, 12194497, 32960380, 1459310),
            ),
        ),
        array(
            array(
                array(19852034, 7027924, 23669353, 10020366, 8586503, -6657907, 394197, -6101885, 18638003, -11174937),
                array(31395534, 15098109, 26581030, 8030562, -16527914, -5007134, 9012486, -7584354, -6643087, -5442636),
                array(-9192165, -2347377, -1997099, 4529534, 25766844, 607986, -13222, 9677543, -32294889, -6456008),
            ),
            array(
                array(-2444496, -149937, 29348902, 8186665, 1873760, 12489863, -30934579, -7839692, -7852844, -8138429),
                array(-15236356, -15433509, 7766470, 746860, 26346930, -10221762, -27333451, 10754588, -9431476, 5203576),
                array(31834314, 14135496, -770007, 5159118, 20917671, -16768096, -7467973, -7337524, 31809243, 7347066),
            ),
            array(
                array(-9606723, -11874240, 20414459, 13033986, 13716524, -11691881, 19797970, -12211255, 15192876, -2087490),
                array(-12663563, -2181719, 1168162, -3804809, 26747877, -14138091, 10609330, 12694420, 33473243, -13382104),
                array(33184999, 11180355, 15832085, -11385430, -1633671, 225884, 15089336, -11023903, -6135662, 14480053),
            ),
            array(
                array(31308717, -5619998, 31030840, -1897099, 15674547, -6582883, 5496208, 13685227, 27595050, 8737275),
                array(-20318852, -15150239, 10933843, -16178022, 8335352, -7546022, -31008351, -12610604, 26498114, 66511),
                array(22644454, -8761729, -16671776, 4884562, -3105614, -13559366, 30540766, -4286747, -13327787, -7515095),
            ),
            array(
                array(-28017847, 9834845, 18617207, -2681312, -3401956, -13307506, 8205540, 13585437, -17127465, 15115439),
                array(23711543, -672915, 31206561, -8362711, 6164647, -9709987, -33535882, -1426096, 8236921, 16492939),
                array(-23910559, -13515526, -26299483, -4503841, 25005590, -7687270, 19574902, 10071562, 6708380, -6222424),
            ),
            array(
                array(2101391, -4930054, 19702731, 2367575, -15427167, 1047675, 5301017, 9328700, 29955601, -11678310),
                array(3096359, 9271816, -21620864, -15521844, -14847996, -7592937, -25892142, -12635595, -9917575, 6216608),
                array(-32615849, 338663, -25195611, 2510422, -29213566, -13820213, 24822830, -6146567, -26767480, 7525079),
            ),
            array(
                array(-23066649, -13985623, 16133487, -7896178, -3389565, 778788, -910336, -2782495, -19386633, 11994101),
                array(21691500, -13624626, -641331, -14367021, 3285881, -3483596, -25064666, 9718258, -7477437, 13381418),
                array(18445390, -4202236, 14979846, 11622458, -1727110, -3582980, 23111648, -6375247, 28535282, 15779576),
            ),
            array(
                array(30098053, 3089662, -9234387, 16662135, -21306940, 11308411, -14068454, 12021730, 9955285, -16303356),
                array(9734894, -14576830, -7473633, -9138735, 2060392, 11313496, -18426029, 9924399, 20194861, 13380996),
                array(-26378102, -7965207, -22167821, 15789297, -18055342, -6168792, -1984914, 15707771, 26342023, 10146099),
            ),
        ),
        array(
            array(
                array(-26016874, -219943, 21339191, -41388, 19745256, -2878700, -29637280, 2227040, 21612326, -545728),
                array(-13077387, 1184228, 23562814, -5970442, -20351244, -6348714, 25764461, 12243797, -20856566, 11649658),
                array(-10031494, 11262626, 27384172, 2271902, 26947504, -15997771, 39944, 6114064, 33514190, 2333242),
            ),
            array(
                array(-21433588, -12421821, 8119782, 7219913, -21830522, -9016134, -6679750, -12670638, 24350578, -13450001),
                array(-4116307, -11271533, -23886186, 4843615, -30088339, 690623, -31536088, -10406836, 8317860, 12352766),
                array(18200138, -14475911, -33087759, -2696619, -23702521, -9102511, -23552096, -2287550, 20712163, 6719373),
            ),
            array(
                array(26656208, 6075253, -7858556, 1886072, -28344043, 4262326, 11117530, -3763210, 26224235, -3297458),
                array(-17168938, -14854097, -3395676, -16369877, -19954045, 14050420, 21728352, 9493610, 18620611, -16428628),
                array(-13323321, 13325349, 11432106, 5964811, 18609221, 6062965, -5269471, -9725556, -30701573, -16479657),
            ),
            array(
                array(-23860538, -11233159, 26961357, 1640861, -32413112, -16737940, 12248509, -5240639, 13735342, 1934062),
                array(25089769, 6742589, 17081145, -13406266, 21909293, -16067981, -15136294, -3765346, -21277997, 5473616),
                array(31883677, -7961101, 1083432, -11572403, 22828471, 13290673, -7125085, 12469656, 29111212, -5451014),
            ),
            array(
                array(24244947, -15050407, -26262976, 2791540, -14997599, 16666678, 24367466, 6388839, -10295587, 452383),
                array(-25640782, -3417841, 5217916, 16224624, 19987036, -4082269, -24236251, -5915248, 15766062, 8407814),
                array(-20406999, 13990231, 15495425, 16395525, 5377168, 15166495, -8917023, -4388953, -8067909, 2276718),
            ),
            array(
                array(30157918, 12924066, -17712050, 9245753, 19895028, 3368142, -23827587, 5096219, 22740376, -7303417),
                array(2041139, -14256350, 7783687, 13876377, -25946985, -13352459, 24051124, 13742383, -15637599, 13295222),
                array(33338237, -8505733, 12532113, 7977527, 9106186, -1715251, -17720195, -4612972, -4451357, -14669444),
            ),
            array(
                array(-20045281, 5454097, -14346548, 6447146, 28862071, 1883651, -2469266, -4141880, 7770569, 9620597),
                array(23208068, 7979712, 33071466, 8149229, 1758231, -10834995, 30945528, -1694323, -33502340, -14767970),
                array(1439958, -16270480, -1079989, -793782, 4625402, 10647766, -5043801, 1220118, 30494170, -11440799),
            ),
            array(
                array(-5037580, -13028295, -2970559, -3061767, 15640974, -6701666, -26739026, 926050, -1684339, -13333647),
                array(13908495, -3549272, 30919928, -6273825, -21521863, 7989039, 9021034, 9078865, 3353509, 4033511),
                array(-29663431, -15113610, 32259991, -344482, 24295849, -12912123, 23161163, 8839127, 27485041, 7356032),
            ),
        ),
        array(
            array(
                array(9661027, 705443, 11980065, -5370154, -1628543, 14661173, -6346142, 2625015, 28431036, -16771834),
                array(-23839233, -8311415, -25945511, 7480958, -17681669, -8354183, -22545972, 14150565, 15970762, 4099461),
                array(29262576, 16756590, 26350592, -8793563, 8529671, -11208050, 13617293, -9937143, 11465739, 8317062),
            ),
            array(
                array(-25493081, -6962928, 32500200, -9419051, -23038724, -2302222, 14898637, 3848455, 20969334, -5157516),
                array(-20384450, -14347713, -18336405, 13884722, -33039454, 2842114, -21610826, -3649888, 11177095, 14989547),
                array(-24496721, -11716016, 16959896, 2278463, 12066309, 10137771, 13515641, 2581286, -28487508, 9930240),
            ),
            array(
                array(-17751622, -2097826, 16544300, -13009300, -15914807, -14949081, 18345767, -13403753, 16291481, -5314038),
                array(-33229194, 2553288, 32678213, 9875984, 8534129, 6889387, -9676774, 6957617, 4368891, 9788741),
                array(16660756, 7281060, -10830758, 12911820, 20108584, -8101676, -21722536, -8613148, 16250552, -11111103),
            ),
            array(
                array(-19765507, 2390526, -16551031, 14161980, 1905286, 6414907, 4689584, 10604807, -30190403, 4782747),
                array(-1354539, 14736941, -7367442, -13292886, 7710542, -14155590, -9981571, 4383045, 22546403, 437323),
                array(31665577, -12180464, -16186830, 1491339, -18368625, 3294682, 27343084, 2786261, -30633590, -14097016),
            ),
            array(
                array(-14467279, -683715, -33374107, 7448552, 19294360, 14334329, -19690631, 2355319, -19284671, -6114373),
                array(15121312, -15796162, 6377020, -6031361, -10798111, -12957845, 18952177, 15496498, -29380133, 11754228),
                array(-2637277, -13483075, 8488727, -14303896, 12728761, -1622493, 7141596, 11724556, 22761615, -10134141),
            ),
            array(
                array(16918416, 11729663, -18083579, 3022987, -31015732, -13339659, -28741185, -12227393, 32851222, 11717399),
                array(11166634, 7338049, -6722523, 4531520, -29468672, -7302055, 31474879, 3483633, -1193175, -4030831),
                array(-185635, 9921305, 31456609, -13536438, -12013818, 13348923, 33142652, 6546660, -19985279, -3948376),
            ),
            array(
                array(-32460596, 11266712, -11197107, -7899103, 31703694, 3855903, -8537131, -12833048, -30772034, -15486313),
                array(-18006477, 12709068, 3991746, -6479188, -21491523, -10550425, -31135347, -16049879, 10928917, 3011958),
                array(-6957757, -15594337, 31696059, 334240, 29576716, 14796075, -30831056, -12805180, 18008031, 10258577),
            ),
            array(
                array(-22448644, 15655569, 7018479, -4410003, -30314266, -1201591, -1853465, 1367120, 25127874, 6671743),
                array(29701166, -14373934, -10878120, 9279288, -17568, 13127210, 21382910, 11042292, 25838796, 4642684),
                array(-20430234, 14955537, -24126347, 8124619, -5369288, -5990470, 30468147, -13900640, 18423289, 4177476),
            ),
        )
    );

    /**
     * See: libsodium's crypto_core/curve25519/ref10/base2.h
     *
     * @var array basically int[8][3]
     */
    protected static $base2 = array(
        array(
            array(25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605),
            array(-12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378),
            array(-8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546),
        ),
        array(
            array(15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024),
            array(16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574),
            array(30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357),
        ),
        array(
            array(10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380),
            array(4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306),
            array(19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942),
        ),
        array(
            array(5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766),
            array(-30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701),
            array(28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300),
        ),
        array(
            array(-22518993, -6692182, 14201702, -8745502, -23510406, 8844726, 18474211, -1361450, -13062696, 13821877),
            array(-6455177, -7839871, 3374702, -4740862, -27098617, -10571707, 31655028, -7212327, 18853322, -14220951),
            array(4566830, -12963868, -28974889, -12240689, -7602672, -2830569, -8514358, -10431137, 2207753, -3209784),
        ),
        array(
            array(-25154831, -4185821, 29681144, 7868801, -6854661, -9423865, -12437364, -663000, -31111463, -16132436),
            array(25576264, -2703214, 7349804, -11814844, 16472782, 9300885, 3844789, 15725684, 171356, 6466918),
            array(23103977, 13316479, 9739013, -16149481, 817875, -15038942, 8965339, -14088058, -30714912, 16193877),
        ),
        array(
            array(-33521811, 3180713, -2394130, 14003687, -16903474, -16270840, 17238398, 4729455, -18074513, 9256800),
            array(-25182317, -4174131, 32336398, 5036987, -21236817, 11360617, 22616405, 9761698, -19827198, 630305),
            array(-13720693, 2639453, -24237460, -7406481, 9494427, -5774029, -6554551, -15960994, -2449256, -14291300),
        ),
        array(
            array(-3151181, -5046075, 9282714, 6866145, -31907062, -863023, -18940575, 15033784, 25105118, -7894876),
            array(-24326370, 15950226, -31801215, -14592823, -11662737, -5090925, 1573892, -2625887, 2198790, -15804619),
            array(-3099351, 10324967, -2241613, 7453183, -5446979, -2735503, -13812022, -16236442, -32461234, -12290683),
        )
    );

    /**
     * 37095705934669439343138083508754565189542113879843219016388785533085940283555
     *
     * @var array<int, int>
     */
    protected static $d = array(
        -10913610,
        13857413,
        -15372611,
        6949391,
        114729,
        -8787816,
        -6275908,
        -3247719,
        -18696448,
        -12055116
    );

    /**
     * 2 * d = 16295367250680780974490674513165176452449235426866156013048779062215315747161
     *
     * @var array<int, int>
     */
    protected static $d2 = array(
        -21827239,
        -5839606,
        -30745221,
        13898782,
        229458,
        15978800,
        -12551817,
        -6495438,
        29715968,
        9444199
    );

    /**
     * sqrt(-1)
     *
     * @var array<int, int>
     */
    protected static $sqrtm1 = array(
        -32595792,
        -7943725,
        9377950,
        3500415,
        12389472,
        -272473,
        -25146209,
        -2005654,
        326686,
        11406482
    );

    /**
     * 1 / sqrt(a - d)
     *
     * @var array<int, int>
     */
    protected static $invsqrtamd = array(
        6111485,
        4156064,
        -27798727,
        12243468,
        -25904040,
        120897,
        20826367,
        -7060776,
        6093568,
        -1986012
    );

    /**
     *  sqrt(ad - 1) with a = -1 (mod p)
     *
     * @var array<int, int>
     */
    protected static $sqrtadm1 = array(
        24849947,
        -153582,
        -23613485,
        6347715,
        -21072328,
        -667138,
        -25271143,
        -15367704,
        -870347,
        14525639
    );

    /**
     * 1 - d ^ 2
     *
     * @var array<int, int>
     */
    protected static $onemsqd = array(
        6275446,
        -16617371,
        -22938544,
        -3773710,
        11667077,
        7397348,
        -27922721,
        1766195,
        -24433858,
        672203
    );

    /**
     * (d - 1) ^ 2
     * @var array<int, int>
     */
    protected static $sqdmone = array(
        15551795,
        -11097455,
        -13425098,
        -10125071,
        -11896535,
        10178284,
        -26634327,
        4729244,
        -5282110,
        -10116402
    );


    /*
     *  2^252+27742317777372353535851937790883648493
        static const unsigned char L[] = {
            0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7,
            0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10
        };
    */
    const L = "\xed\xd3\xf5\x5c\x1a\x63\x12\x58\xd6\x9c\xf7\xa2\xde\xf9\xde\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10";
}
Core/XChaCha20.php000064400000006452152331075510007557 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_XChaCha20', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_XChaCha20
 */
class ParagonIE_Sodium_Core_XChaCha20 extends ParagonIE_Sodium_Core_HChaCha20
{
    /**
     * @internal You should not use this directly from another application
     *
     * @param int $len
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function stream($len = 64, $nonce = '', $key = '')
    {
        if (self::strlen($nonce) !== 24) {
            throw new SodiumException('Nonce must be 24 bytes long');
        }
        return self::encryptBytes(
            new ParagonIE_Sodium_Core_ChaCha20_Ctx(
                self::hChaCha20(
                    self::substr($nonce, 0, 16),
                    $key
                ),
                self::substr($nonce, 16, 8)
            ),
            str_repeat("\x00", $len)
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $len
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ietfStream($len = 64, $nonce = '', $key = '')
    {
        if (self::strlen($nonce) !== 24) {
            throw new SodiumException('Nonce must be 24 bytes long');
        }
        return self::encryptBytes(
            new ParagonIE_Sodium_Core_ChaCha20_IetfCtx(
                self::hChaCha20(
                    self::substr($nonce, 0, 16),
                    $key
                ),
                "\x00\x00\x00\x00" . self::substr($nonce, 16, 8)
            ),
            str_repeat("\x00", $len)
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $nonce
     * @param string $key
     * @param string $ic
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function streamXorIc($message, $nonce = '', $key = '', $ic = '')
    {
        if (self::strlen($nonce) !== 24) {
            throw new SodiumException('Nonce must be 24 bytes long');
        }
        return self::encryptBytes(
            new ParagonIE_Sodium_Core_ChaCha20_Ctx(
                self::hChaCha20(self::substr($nonce, 0, 16), $key),
                self::substr($nonce, 16, 8),
                $ic
            ),
            $message
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $nonce
     * @param string $key
     * @param string $ic
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ietfStreamXorIc($message, $nonce = '', $key = '', $ic = '')
    {
        if (self::strlen($nonce) !== 24) {
            throw new SodiumException('Nonce must be 24 bytes long');
        }
        return self::encryptBytes(
            new ParagonIE_Sodium_Core_ChaCha20_IetfCtx(
                self::hChaCha20(self::substr($nonce, 0, 16), $key),
                "\x00\x00\x00\x00" . self::substr($nonce, 16, 8),
                $ic
            ),
            $message
        );
    }
}
Core/Ed25519.php000064400000041722152331075510007113 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_Ed25519', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_Ed25519
 */
abstract class ParagonIE_Sodium_Core_Ed25519 extends ParagonIE_Sodium_Core_Curve25519
{
    const KEYPAIR_BYTES = 96;
    const SEED_BYTES = 32;
    const SCALAR_BYTES = 32;

    /**
     * @internal You should not use this directly from another application
     *
     * @return string (96 bytes)
     * @throws Exception
     * @throws SodiumException
     * @throws TypeError
     */
    public static function keypair()
    {
        $seed = random_bytes(self::SEED_BYTES);
        $pk = '';
        $sk = '';
        self::seed_keypair($pk, $sk, $seed);
        return $sk . $pk;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $pk
     * @param string $sk
     * @param string $seed
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function seed_keypair(&$pk, &$sk, $seed)
    {
        if (self::strlen($seed) !== self::SEED_BYTES) {
            throw new RangeException('crypto_sign keypair seed must be 32 bytes long');
        }

        /** @var string $pk */
        $pk = self::publickey_from_secretkey($seed);
        $sk = $seed . $pk;
        return $sk;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $keypair
     * @return string
     * @throws TypeError
     */
    public static function secretkey($keypair)
    {
        if (self::strlen($keypair) !== self::KEYPAIR_BYTES) {
            throw new RangeException('crypto_sign keypair must be 96 bytes long');
        }
        return self::substr($keypair, 0, 64);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $keypair
     * @return string
     * @throws TypeError
     */
    public static function publickey($keypair)
    {
        if (self::strlen($keypair) !== self::KEYPAIR_BYTES) {
            throw new RangeException('crypto_sign keypair must be 96 bytes long');
        }
        return self::substr($keypair, 64, 32);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function publickey_from_secretkey($sk)
    {
        /** @var string $sk */
        $sk = hash('sha512', self::substr($sk, 0, 32), true);
        $sk[0] = self::intToChr(
            self::chrToInt($sk[0]) & 248
        );
        $sk[31] = self::intToChr(
            (self::chrToInt($sk[31]) & 63) | 64
        );
        return self::sk_to_pk($sk);
    }

    /**
     * @param string $pk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function pk_to_curve25519($pk)
    {
        if (self::small_order($pk)) {
            throw new SodiumException('Public key is on a small order');
        }
        $A = self::ge_frombytes_negate_vartime(self::substr($pk, 0, 32));
        $p1 = self::ge_mul_l($A);
        if (!self::fe_isnonzero($p1->X)) {
            throw new SodiumException('Unexpected zero result');
        }

        # fe_1(one_minus_y);
        # fe_sub(one_minus_y, one_minus_y, A.Y);
        # fe_invert(one_minus_y, one_minus_y);
        $one_minux_y = self::fe_invert(
            self::fe_sub(
                self::fe_1(),
                $A->Y
            )
        );

        # fe_1(x);
        # fe_add(x, x, A.Y);
        # fe_mul(x, x, one_minus_y);
        $x = self::fe_mul(
            self::fe_add(self::fe_1(), $A->Y),
            $one_minux_y
        );

        # fe_tobytes(curve25519_pk, x);
        return self::fe_tobytes($x);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sk_to_pk($sk)
    {
        return self::ge_p3_tobytes(
            self::ge_scalarmult_base(
                self::substr($sk, 0, 32)
            )
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign($message, $sk)
    {
        /** @var string $signature */
        $signature = self::sign_detached($message, $sk);
        return $signature . $message;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message A signed message
     * @param string $pk      Public key
     * @return string         Message (without signature)
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign_open($message, $pk)
    {
        /** @var string $signature */
        $signature = self::substr($message, 0, 64);

        /** @var string $message */
        $message = self::substr($message, 64);

        if (self::verify_detached($signature, $message, $pk)) {
            return $message;
        }
        throw new SodiumException('Invalid signature');
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign_detached($message, $sk)
    {
        # crypto_hash_sha512(az, sk, 32);
        $az =  hash('sha512', self::substr($sk, 0, 32), true);

        # az[0] &= 248;
        # az[31] &= 63;
        # az[31] |= 64;
        $az[0] = self::intToChr(self::chrToInt($az[0]) & 248);
        $az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64);

        # crypto_hash_sha512_init(&hs);
        # crypto_hash_sha512_update(&hs, az + 32, 32);
        # crypto_hash_sha512_update(&hs, m, mlen);
        # crypto_hash_sha512_final(&hs, nonce);
        $hs = hash_init('sha512');
        hash_update($hs, self::substr($az, 32, 32));
        hash_update($hs, $message);
        $nonceHash = hash_final($hs, true);

        # memmove(sig + 32, sk + 32, 32);
        $pk = self::substr($sk, 32, 32);

        # sc_reduce(nonce);
        # ge_scalarmult_base(&R, nonce);
        # ge_p3_tobytes(sig, &R);
        $nonce = self::sc_reduce($nonceHash) . self::substr($nonceHash, 32);
        $sig = self::ge_p3_tobytes(
            self::ge_scalarmult_base($nonce)
        );

        # crypto_hash_sha512_init(&hs);
        # crypto_hash_sha512_update(&hs, sig, 64);
        # crypto_hash_sha512_update(&hs, m, mlen);
        # crypto_hash_sha512_final(&hs, hram);
        $hs = hash_init('sha512');
        hash_update($hs, self::substr($sig, 0, 32));
        hash_update($hs, self::substr($pk, 0, 32));
        hash_update($hs, $message);
        $hramHash = hash_final($hs, true);

        # sc_reduce(hram);
        # sc_muladd(sig + 32, hram, az, nonce);
        $hram = self::sc_reduce($hramHash);
        $sigAfter = self::sc_muladd($hram, $az, $nonce);
        $sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32);

        try {
            ParagonIE_Sodium_Compat::memzero($az);
        } catch (SodiumException $ex) {
            $az = null;
        }
        return $sig;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $sig
     * @param string $message
     * @param string $pk
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function verify_detached($sig, $message, $pk)
    {
        if (self::strlen($sig) < 64) {
            throw new SodiumException('Signature is too short');
        }
        if ((self::chrToInt($sig[63]) & 240) && self::check_S_lt_L(self::substr($sig, 32, 32))) {
            throw new SodiumException('S < L - Invalid signature');
        }
        if (self::small_order($sig)) {
            throw new SodiumException('Signature is on too small of an order');
        }
        if ((self::chrToInt($sig[63]) & 224) !== 0) {
            throw new SodiumException('Invalid signature');
        }
        $d = 0;
        for ($i = 0; $i < 32; ++$i) {
            $d |= self::chrToInt($pk[$i]);
        }
        if ($d === 0) {
            throw new SodiumException('All zero public key');
        }

        /** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */
        $orig = ParagonIE_Sodium_Compat::$fastMult;

        // Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification.
        ParagonIE_Sodium_Compat::$fastMult = true;

        /** @var ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A */
        $A = self::ge_frombytes_negate_vartime($pk);

        /** @var string $hDigest */
        $hDigest = hash(
            'sha512',
            self::substr($sig, 0, 32) .
                self::substr($pk, 0, 32) .
                $message,
            true
        );

        /** @var string $h */
        $h = self::sc_reduce($hDigest) . self::substr($hDigest, 32);

        /** @var ParagonIE_Sodium_Core_Curve25519_Ge_P2 $R */
        $R = self::ge_double_scalarmult_vartime(
            $h,
            $A,
            self::substr($sig, 32)
        );

        /** @var string $rcheck */
        $rcheck = self::ge_tobytes($R);

        // Reset ParagonIE_Sodium_Compat::$fastMult to what it was before.
        ParagonIE_Sodium_Compat::$fastMult = $orig;

        return self::verify_32($rcheck, self::substr($sig, 0, 32));
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $S
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function check_S_lt_L($S)
    {
        if (self::strlen($S) < 32) {
            throw new SodiumException('Signature must be 32 bytes');
        }
        $L = array(
            0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,
            0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10
        );
        $c = 0;
        $n = 1;
        $i = 32;

        /** @var array<int, int> $L */
        do {
            --$i;
            $x = self::chrToInt($S[$i]);
            $c |= (
                (($x - $L[$i]) >> 8) & $n
            );
            $n &= (
                (($x ^ $L[$i]) - 1) >> 8
            );
        } while ($i !== 0);

        return $c === 0;
    }

    /**
     * @param string $R
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function small_order($R)
    {
        /** @var array<int, array<int, int>> $blocklist */
        $blocklist = array(
            /* 0 (order 4) */
            array(
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
            ),
            /* 1 (order 1) */
            array(
                0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
            ),
            /* 2707385501144840649318225287225658788936804267575313519463743609750303402022 (order 8) */
            array(
                0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0,
                0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98, 0xf0,
                0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39,
                0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05
            ),
            /* 55188659117513257062467267217118295137698188065244968500265048394206261417927 (order 8) */
            array(
                0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f,
                0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, 0x0f,
                0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6,
                0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a
            ),
            /* p-1 (order 2) */
            array(
                0x13, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0,
                0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98, 0xf0,
                0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39,
                0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x85
            ),
            /* p (order 4) */
            array(
                0xb4, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f,
                0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, 0x0f,
                0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6,
                0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0xfa
            ),
            /* p+1 (order 1) */
            array(
                0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f
            ),
            /* p+2707385501144840649318225287225658788936804267575313519463743609750303402022 (order 8) */
            array(
                0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f
            ),
            /* p+55188659117513257062467267217118295137698188065244968500265048394206261417927 (order 8) */
            array(
                0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f
            ),
            /* 2p-1 (order 2) */
            array(
                0xd9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
            ),
            /* 2p (order 4) */
            array(
                0xda, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
            ),
            /* 2p+1 (order 1) */
            array(
                0xdb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
            )
        );
        /** @var int $countBlocklist */
        $countBlocklist = count($blocklist);

        for ($i = 0; $i < $countBlocklist; ++$i) {
            $c = 0;
            for ($j = 0; $j < 32; ++$j) {
                $c |= self::chrToInt($R[$j]) ^ (int) $blocklist[$i][$j];
            }
            if ($c === 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param string $s
     * @return string
     * @throws SodiumException
     */
    public static function scalar_complement($s)
    {
        $t_ = self::L . str_repeat("\x00", 32);
        sodium_increment($t_);
        $s_ = $s . str_repeat("\x00", 32);
        ParagonIE_Sodium_Compat::sub($t_, $s_);
        return self::sc_reduce($t_);
    }

    /**
     * @return string
     * @throws SodiumException
     */
    public static function scalar_random()
    {
        do {
            $r = ParagonIE_Sodium_Compat::randombytes_buf(self::SCALAR_BYTES);
            $r[self::SCALAR_BYTES - 1] = self::intToChr(
                self::chrToInt($r[self::SCALAR_BYTES - 1]) & 0x1f
            );
        } while (
            !self::check_S_lt_L($r) || ParagonIE_Sodium_Compat::is_zero($r)
        );
        return $r;
    }

    /**
     * @param string $s
     * @return string
     * @throws SodiumException
     */
    public static function scalar_negate($s)
    {
        $t_ = self::L . str_repeat("\x00", 32) ;
        $s_ = $s . str_repeat("\x00", 32) ;
        ParagonIE_Sodium_Compat::sub($t_, $s_);
        return self::sc_reduce($t_);
    }

    /**
     * @param string $a
     * @param string $b
     * @return string
     * @throws SodiumException
     */
    public static function scalar_add($a, $b)
    {
        $a_ = $a . str_repeat("\x00", 32);
        $b_ = $b . str_repeat("\x00", 32);
        ParagonIE_Sodium_Compat::add($a_, $b_);
        return self::sc_reduce($a_);
    }

    /**
     * @param string $x
     * @param string $y
     * @return string
     * @throws SodiumException
     */
    public static function scalar_sub($x, $y)
    {
        $yn = self::scalar_negate($y);
        return self::scalar_add($x, $yn);
    }
}
Core/Poly1305/State.php000064400000031160152331075510010464 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_Poly1305_State', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_Poly1305_State
 */
class ParagonIE_Sodium_Core_Poly1305_State extends ParagonIE_Sodium_Core_Util
{
    /**
     * @var array<int, int>
     */
    protected $buffer = array();

    /**
     * @var bool
     */
    protected $final = false;

    /**
     * @var array<int, int>
     */
    public $h;

    /**
     * @var int
     */
    protected $leftover = 0;

    /**
     * @var int[]
     */
    public $r;

    /**
     * @var int[]
     */
    public $pad;

    /**
     * ParagonIE_Sodium_Core_Poly1305_State constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $key
     * @throws InvalidArgumentException
     * @throws TypeError
     */
    public function __construct($key = '')
    {
        if (self::strlen($key) < 32) {
            throw new InvalidArgumentException(
                'Poly1305 requires a 32-byte key'
            );
        }
        /* r &= 0xffffffc0ffffffc0ffffffc0fffffff */
        $this->r = array(
            (int) ((self::load_4(self::substr($key, 0, 4))) & 0x3ffffff),
            (int) ((self::load_4(self::substr($key, 3, 4)) >> 2) & 0x3ffff03),
            (int) ((self::load_4(self::substr($key, 6, 4)) >> 4) & 0x3ffc0ff),
            (int) ((self::load_4(self::substr($key, 9, 4)) >> 6) & 0x3f03fff),
            (int) ((self::load_4(self::substr($key, 12, 4)) >> 8) & 0x00fffff)
        );

        /* h = 0 */
        $this->h = array(0, 0, 0, 0, 0);

        /* save pad for later */
        $this->pad = array(
            self::load_4(self::substr($key, 16, 4)),
            self::load_4(self::substr($key, 20, 4)),
            self::load_4(self::substr($key, 24, 4)),
            self::load_4(self::substr($key, 28, 4)),
        );

        $this->leftover = 0;
        $this->final = false;
    }

    /**
     * Zero internal buffer upon destruction
     */
    public function __destruct()
    {
        $this->r[0] ^= $this->r[0];
        $this->r[1] ^= $this->r[1];
        $this->r[2] ^= $this->r[2];
        $this->r[3] ^= $this->r[3];
        $this->r[4] ^= $this->r[4];
        $this->h[0] ^= $this->h[0];
        $this->h[1] ^= $this->h[1];
        $this->h[2] ^= $this->h[2];
        $this->h[3] ^= $this->h[3];
        $this->h[4] ^= $this->h[4];
        $this->pad[0] ^= $this->pad[0];
        $this->pad[1] ^= $this->pad[1];
        $this->pad[2] ^= $this->pad[2];
        $this->pad[3] ^= $this->pad[3];
        $this->leftover = 0;
        $this->final = true;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @return self
     * @throws SodiumException
     * @throws TypeError
     */
    public function update($message = '')
    {
        $bytes = self::strlen($message);
        if ($bytes < 1) {
            return $this;
        }

        /* handle leftover */
        if ($this->leftover) {
            $want = ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE - $this->leftover;
            if ($want > $bytes) {
                $want = $bytes;
            }
            for ($i = 0; $i < $want; ++$i) {
                $mi = self::chrToInt($message[$i]);
                $this->buffer[$this->leftover + $i] = $mi;
            }
            // We snip off the leftmost bytes.
            $message = self::substr($message, $want);
            $bytes = self::strlen($message);
            $this->leftover += $want;
            if ($this->leftover < ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE) {
                // We still don't have enough to run $this->blocks()
                return $this;
            }

            $this->blocks(
                self::intArrayToString($this->buffer),
                ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE
            );
            $this->leftover = 0;
        }

        /* process full blocks */
        if ($bytes >= ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE) {
            /** @var int $want */
            $want = $bytes & ~(ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE - 1);
            if ($want >= ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE) {
                $block = self::substr($message, 0, $want);
                if (self::strlen($block) >= ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE) {
                    $this->blocks($block, $want);
                    $message = self::substr($message, $want);
                    $bytes = self::strlen($message);
                }
            }
        }

        /* store leftover */
        if ($bytes) {
            for ($i = 0; $i < $bytes; ++$i) {
                $mi = self::chrToInt($message[$i]);
                $this->buffer[$this->leftover + $i] = $mi;
            }
            $this->leftover = (int) $this->leftover + $bytes;
        }
        return $this;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param int $bytes
     * @return self
     * @throws TypeError
     */
    public function blocks($message, $bytes)
    {
        if (self::strlen($message) < 16) {
            $message = str_pad($message, 16, "\x00", STR_PAD_RIGHT);
        }
        /** @var int $hibit */
        $hibit = $this->final ? 0 : 1 << 24; /* 1 << 128 */
        $r0 = (int) $this->r[0];
        $r1 = (int) $this->r[1];
        $r2 = (int) $this->r[2];
        $r3 = (int) $this->r[3];
        $r4 = (int) $this->r[4];

        $s1 = self::mul($r1, 5, 3);
        $s2 = self::mul($r2, 5, 3);
        $s3 = self::mul($r3, 5, 3);
        $s4 = self::mul($r4, 5, 3);

        $h0 = $this->h[0];
        $h1 = $this->h[1];
        $h2 = $this->h[2];
        $h3 = $this->h[3];
        $h4 = $this->h[4];

        while ($bytes >= ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE) {
            /* h += m[i] */
            $h0 +=  self::load_4(self::substr($message, 0, 4))       & 0x3ffffff;
            $h1 += (self::load_4(self::substr($message, 3, 4)) >> 2) & 0x3ffffff;
            $h2 += (self::load_4(self::substr($message, 6, 4)) >> 4) & 0x3ffffff;
            $h3 += (self::load_4(self::substr($message, 9, 4)) >> 6) & 0x3ffffff;
            $h4 += (self::load_4(self::substr($message, 12, 4)) >> 8) | $hibit;

            /* h *= r */
            $d0 = (
                self::mul($h0, $r0, 25) +
                self::mul($s4, $h1, 26) +
                self::mul($s3, $h2, 26) +
                self::mul($s2, $h3, 26) +
                self::mul($s1, $h4, 26)
            );

            $d1 = (
                self::mul($h0, $r1, 25) +
                self::mul($h1, $r0, 25) +
                self::mul($s4, $h2, 26) +
                self::mul($s3, $h3, 26) +
                self::mul($s2, $h4, 26)
            );

            $d2 = (
                self::mul($h0, $r2, 25) +
                self::mul($h1, $r1, 25) +
                self::mul($h2, $r0, 25) +
                self::mul($s4, $h3, 26) +
                self::mul($s3, $h4, 26)
            );

            $d3 = (
                self::mul($h0, $r3, 25) +
                self::mul($h1, $r2, 25) +
                self::mul($h2, $r1, 25) +
                self::mul($h3, $r0, 25) +
                self::mul($s4, $h4, 26)
            );

            $d4 = (
                self::mul($h0, $r4, 25) +
                self::mul($h1, $r3, 25) +
                self::mul($h2, $r2, 25) +
                self::mul($h3, $r1, 25) +
                self::mul($h4, $r0, 25)
            );

            /* (partial) h %= p */
            /** @var int $c */
            $c = $d0 >> 26;
            /** @var int $h0 */
            $h0 = $d0 & 0x3ffffff;
            $d1 += $c;

            /** @var int $c */
            $c = $d1 >> 26;
            /** @var int $h1 */
            $h1 = $d1 & 0x3ffffff;
            $d2 += $c;

            /** @var int $c */
            $c = $d2 >> 26;
            /** @var int $h2  */
            $h2 = $d2 & 0x3ffffff;
            $d3 += $c;

            /** @var int $c */
            $c = $d3 >> 26;
            /** @var int $h3 */
            $h3 = $d3 & 0x3ffffff;
            $d4 += $c;

            /** @var int $c */
            $c = $d4 >> 26;
            /** @var int $h4 */
            $h4 = $d4 & 0x3ffffff;
            $h0 += (int) self::mul($c, 5, 3);

            /** @var int $c */
            $c = $h0 >> 26;
            /** @var int $h0 */
            $h0 &= 0x3ffffff;
            $h1 += $c;

            // Chop off the left 32 bytes.
            $message = self::substr(
                $message,
                ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE
            );
            $bytes -= ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE;
        }

        $this->h = array(
            (int) ($h0 & 0xffffffff),
            (int) ($h1 & 0xffffffff),
            (int) ($h2 & 0xffffffff),
            (int) ($h3 & 0xffffffff),
            (int) ($h4 & 0xffffffff)
        );
        return $this;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @return string
     * @throws TypeError
     */
    public function finish()
    {
        /* process the remaining block */
        if ($this->leftover) {
            $i = $this->leftover;
            $this->buffer[$i++] = 1;
            for (; $i < ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE; ++$i) {
                $this->buffer[$i] = 0;
            }
            $this->final = true;
            $this->blocks(
                self::substr(
                    self::intArrayToString($this->buffer),
                    0,
                    ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE
                ),
                ParagonIE_Sodium_Core_Poly1305::BLOCK_SIZE
            );
        }

        $h0 = (int) $this->h[0];
        $h1 = (int) $this->h[1];
        $h2 = (int) $this->h[2];
        $h3 = (int) $this->h[3];
        $h4 = (int) $this->h[4];

        /** @var int $c */
        $c = $h1 >> 26;
        /** @var int $h1 */
        $h1 &= 0x3ffffff;
        /** @var int $h2 */
        $h2 += $c;
        /** @var int $c */
        $c = $h2 >> 26;
        /** @var int $h2 */
        $h2 &= 0x3ffffff;
        $h3 += $c;
        /** @var int $c */
        $c = $h3 >> 26;
        $h3 &= 0x3ffffff;
        $h4 += $c;
        /** @var int $c */
        $c = $h4 >> 26;
        $h4 &= 0x3ffffff;
        /** @var int $h0 */
        $h0 += self::mul($c, 5, 3);
        /** @var int $c */
        $c = $h0 >> 26;
        /** @var int $h0 */
        $h0 &= 0x3ffffff;
        /** @var int $h1 */
        $h1 += $c;

        /* compute h + -p */
        /** @var int $g0 */
        $g0 = $h0 + 5;
        /** @var int $c */
        $c = $g0 >> 26;
        /** @var int $g0 */
        $g0 &= 0x3ffffff;

        /** @var int $g1 */
        $g1 = $h1 + $c;
        /** @var int $c */
        $c = $g1 >> 26;
        $g1 &= 0x3ffffff;

        /** @var int $g2 */
        $g2 = $h2 + $c;
        /** @var int $c */
        $c = $g2 >> 26;
        /** @var int $g2 */
        $g2 &= 0x3ffffff;

        /** @var int $g3 */
        $g3 = $h3 + $c;
        /** @var int $c */
        $c = $g3 >> 26;
        /** @var int $g3 */
        $g3 &= 0x3ffffff;

        /** @var int $g4 */
        $g4 = ($h4 + $c - (1 << 26)) & 0xffffffff;

        /* select h if h < p, or h + -p if h >= p */
        /** @var int $mask */
        $mask = ($g4 >> 31) - 1;

        $g0 &= $mask;
        $g1 &= $mask;
        $g2 &= $mask;
        $g3 &= $mask;
        $g4 &= $mask;

        /** @var int $mask */
        $mask = ~$mask & 0xffffffff;
        /** @var int $h0 */
        $h0 = ($h0 & $mask) | $g0;
        /** @var int $h1 */
        $h1 = ($h1 & $mask) | $g1;
        /** @var int $h2 */
        $h2 = ($h2 & $mask) | $g2;
        /** @var int $h3 */
        $h3 = ($h3 & $mask) | $g3;
        /** @var int $h4 */
        $h4 = ($h4 & $mask) | $g4;

        /* h = h % (2^128) */
        /** @var int $h0 */
        $h0 = (($h0) | ($h1 << 26)) & 0xffffffff;
        /** @var int $h1 */
        $h1 = (($h1 >>  6) | ($h2 << 20)) & 0xffffffff;
        /** @var int $h2 */
        $h2 = (($h2 >> 12) | ($h3 << 14)) & 0xffffffff;
        /** @var int $h3 */
        $h3 = (($h3 >> 18) | ($h4 <<  8)) & 0xffffffff;

        /* mac = (h + pad) % (2^128) */
        $f = (int) ($h0 + $this->pad[0]);
        $h0 = (int) $f;
        $f = (int) ($h1 + $this->pad[1] + ($f >> 32));
        $h1 = (int) $f;
        $f = (int) ($h2 + $this->pad[2] + ($f >> 32));
        $h2 = (int) $f;
        $f = (int) ($h3 + $this->pad[3] + ($f >> 32));
        $h3 = (int) $f;

        return self::store32_le($h0 & 0xffffffff) .
            self::store32_le($h1 & 0xffffffff) .
            self::store32_le($h2 & 0xffffffff) .
            self::store32_le($h3 & 0xffffffff);
    }
}
Core/Poly1305.php000064400000003046152331075510007406 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_Poly1305', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_Poly1305
 */
abstract class ParagonIE_Sodium_Core_Poly1305 extends ParagonIE_Sodium_Core_Util
{
    const BLOCK_SIZE = 16;

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $m
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function onetimeauth($m, $key)
    {
        if (self::strlen($key) < 32) {
            throw new InvalidArgumentException(
                'Key must be 32 bytes long.'
            );
        }
        $state = new ParagonIE_Sodium_Core_Poly1305_State(
            self::substr($key, 0, 32)
        );
        return $state
            ->update($m)
            ->finish();
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $mac
     * @param string $m
     * @param string $key
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function onetimeauth_verify($mac, $m, $key)
    {
        if (self::strlen($key) < 32) {
            throw new InvalidArgumentException(
                'Key must be 32 bytes long.'
            );
        }
        $state = new ParagonIE_Sodium_Core_Poly1305_State(
            self::substr($key, 0, 32)
        );
        $calc = $state
            ->update($m)
            ->finish();
        return self::verify_16($calc, $mac);
    }
}
Core/X25519.php000064400000022352152331075510006770 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_X25519', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_X25519
 */
abstract class ParagonIE_Sodium_Core_X25519 extends ParagonIE_Sodium_Core_Curve25519
{
    /**
     * Alters the objects passed to this method in place.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $g
     * @param int $b
     * @return void
     * @psalm-suppress MixedAssignment
     */
    public static function fe_cswap(
        ParagonIE_Sodium_Core_Curve25519_Fe $f,
        ParagonIE_Sodium_Core_Curve25519_Fe $g,
        $b = 0
    ) {
        $f0 = (int) $f[0];
        $f1 = (int) $f[1];
        $f2 = (int) $f[2];
        $f3 = (int) $f[3];
        $f4 = (int) $f[4];
        $f5 = (int) $f[5];
        $f6 = (int) $f[6];
        $f7 = (int) $f[7];
        $f8 = (int) $f[8];
        $f9 = (int) $f[9];
        $g0 = (int) $g[0];
        $g1 = (int) $g[1];
        $g2 = (int) $g[2];
        $g3 = (int) $g[3];
        $g4 = (int) $g[4];
        $g5 = (int) $g[5];
        $g6 = (int) $g[6];
        $g7 = (int) $g[7];
        $g8 = (int) $g[8];
        $g9 = (int) $g[9];
        $b = -$b;
        $x0 = ($f0 ^ $g0) & $b;
        $x1 = ($f1 ^ $g1) & $b;
        $x2 = ($f2 ^ $g2) & $b;
        $x3 = ($f3 ^ $g3) & $b;
        $x4 = ($f4 ^ $g4) & $b;
        $x5 = ($f5 ^ $g5) & $b;
        $x6 = ($f6 ^ $g6) & $b;
        $x7 = ($f7 ^ $g7) & $b;
        $x8 = ($f8 ^ $g8) & $b;
        $x9 = ($f9 ^ $g9) & $b;
        $f[0] = $f0 ^ $x0;
        $f[1] = $f1 ^ $x1;
        $f[2] = $f2 ^ $x2;
        $f[3] = $f3 ^ $x3;
        $f[4] = $f4 ^ $x4;
        $f[5] = $f5 ^ $x5;
        $f[6] = $f6 ^ $x6;
        $f[7] = $f7 ^ $x7;
        $f[8] = $f8 ^ $x8;
        $f[9] = $f9 ^ $x9;
        $g[0] = $g0 ^ $x0;
        $g[1] = $g1 ^ $x1;
        $g[2] = $g2 ^ $x2;
        $g[3] = $g3 ^ $x3;
        $g[4] = $g4 ^ $x4;
        $g[5] = $g5 ^ $x5;
        $g[6] = $g6 ^ $x6;
        $g[7] = $g7 ^ $x7;
        $g[8] = $g8 ^ $x8;
        $g[9] = $g9 ^ $x9;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public static function fe_mul121666(ParagonIE_Sodium_Core_Curve25519_Fe $f)
    {
        $h = array(
            self::mul((int) $f[0], 121666, 17),
            self::mul((int) $f[1], 121666, 17),
            self::mul((int) $f[2], 121666, 17),
            self::mul((int) $f[3], 121666, 17),
            self::mul((int) $f[4], 121666, 17),
            self::mul((int) $f[5], 121666, 17),
            self::mul((int) $f[6], 121666, 17),
            self::mul((int) $f[7], 121666, 17),
            self::mul((int) $f[8], 121666, 17),
            self::mul((int) $f[9], 121666, 17)
        );

        /** @var int $carry9 */
        $carry9 = ($h[9] + (1 << 24)) >> 25;
        $h[0] += self::mul($carry9, 19, 5);
        $h[9] -= $carry9 << 25;
        /** @var int $carry1 */
        $carry1 = ($h[1] + (1 << 24)) >> 25;
        $h[2] += $carry1;
        $h[1] -= $carry1 << 25;
        /** @var int $carry3 */
        $carry3 = ($h[3] + (1 << 24)) >> 25;
        $h[4] += $carry3;
        $h[3] -= $carry3 << 25;
        /** @var int $carry5 */
        $carry5 = ($h[5] + (1 << 24)) >> 25;
        $h[6] += $carry5;
        $h[5] -= $carry5 << 25;
        /** @var int $carry7 */
        $carry7 = ($h[7] + (1 << 24)) >> 25;
        $h[8] += $carry7;
        $h[7] -= $carry7 << 25;

        /** @var int $carry0 */
        $carry0 = ($h[0] + (1 << 25)) >> 26;
        $h[1] += $carry0;
        $h[0] -= $carry0 << 26;
        /** @var int $carry2 */
        $carry2 = ($h[2] + (1 << 25)) >> 26;
        $h[3] += $carry2;
        $h[2] -= $carry2 << 26;
        /** @var int $carry4 */
        $carry4 = ($h[4] + (1 << 25)) >> 26;
        $h[5] += $carry4;
        $h[4] -= $carry4 << 26;
        /** @var int $carry6 */
        $carry6 = ($h[6] + (1 << 25)) >> 26;
        $h[7] += $carry6;
        $h[6] -= $carry6 << 26;
        /** @var int $carry8 */
        $carry8 = ($h[8] + (1 << 25)) >> 26;
        $h[9] += $carry8;
        $h[8] -= $carry8 << 26;

        foreach ($h as $i => $value) {
            $h[$i] = (int) $value;
        }
        return ParagonIE_Sodium_Core_Curve25519_Fe::fromArray($h);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * Inline comments preceded by # are from libsodium's ref10 code.
     *
     * @param string $n
     * @param string $p
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function crypto_scalarmult_curve25519_ref10($n, $p)
    {
        # for (i = 0;i < 32;++i) e[i] = n[i];
        $e = '' . $n;
        # e[0] &= 248;
        $e[0] = self::intToChr(
            self::chrToInt($e[0]) & 248
        );
        # e[31] &= 127;
        # e[31] |= 64;
        $e[31] = self::intToChr(
            (self::chrToInt($e[31]) & 127) | 64
        );
        # fe_frombytes(x1,p);
        $x1 = self::fe_frombytes($p);
        # fe_1(x2);
        $x2 = self::fe_1();
        # fe_0(z2);
        $z2 = self::fe_0();
        # fe_copy(x3,x1);
        $x3 = self::fe_copy($x1);
        # fe_1(z3);
        $z3 = self::fe_1();

        # swap = 0;
        /** @var int $swap */
        $swap = 0;

        # for (pos = 254;pos >= 0;--pos) {
        for ($pos = 254; $pos >= 0; --$pos) {
            # b = e[pos / 8] >> (pos & 7);
            /** @var int $b */
            $b = self::chrToInt(
                    $e[(int) floor($pos / 8)]
                ) >> ($pos & 7);
            # b &= 1;
            $b &= 1;
            # swap ^= b;
            $swap ^= $b;
            # fe_cswap(x2,x3,swap);
            self::fe_cswap($x2, $x3, $swap);
            # fe_cswap(z2,z3,swap);
            self::fe_cswap($z2, $z3, $swap);
            # swap = b;
            $swap = $b;
            # fe_sub(tmp0,x3,z3);
            $tmp0 = self::fe_sub($x3, $z3);
            # fe_sub(tmp1,x2,z2);
            $tmp1 = self::fe_sub($x2, $z2);

            # fe_add(x2,x2,z2);
            $x2 = self::fe_add($x2, $z2);

            # fe_add(z2,x3,z3);
            $z2 = self::fe_add($x3, $z3);

            # fe_mul(z3,tmp0,x2);
            $z3 = self::fe_mul($tmp0, $x2);

            # fe_mul(z2,z2,tmp1);
            $z2 = self::fe_mul($z2, $tmp1);

            # fe_sq(tmp0,tmp1);
            $tmp0 = self::fe_sq($tmp1);

            # fe_sq(tmp1,x2);
            $tmp1 = self::fe_sq($x2);

            # fe_add(x3,z3,z2);
            $x3 = self::fe_add($z3, $z2);

            # fe_sub(z2,z3,z2);
            $z2 = self::fe_sub($z3, $z2);

            # fe_mul(x2,tmp1,tmp0);
            $x2 = self::fe_mul($tmp1, $tmp0);

            # fe_sub(tmp1,tmp1,tmp0);
            $tmp1 = self::fe_sub($tmp1, $tmp0);

            # fe_sq(z2,z2);
            $z2 = self::fe_sq($z2);

            # fe_mul121666(z3,tmp1);
            $z3 = self::fe_mul121666($tmp1);

            # fe_sq(x3,x3);
            $x3 = self::fe_sq($x3);

            # fe_add(tmp0,tmp0,z3);
            $tmp0 = self::fe_add($tmp0, $z3);

            # fe_mul(z3,x1,z2);
            $z3 = self::fe_mul($x1, $z2);

            # fe_mul(z2,tmp1,tmp0);
            $z2 = self::fe_mul($tmp1, $tmp0);
        }

        # fe_cswap(x2,x3,swap);
        self::fe_cswap($x2, $x3, $swap);

        # fe_cswap(z2,z3,swap);
        self::fe_cswap($z2, $z3, $swap);

        # fe_invert(z2,z2);
        $z2 = self::fe_invert($z2);

        # fe_mul(x2,x2,z2);
        $x2 = self::fe_mul($x2, $z2);
        # fe_tobytes(q,x2);
        return self::fe_tobytes($x2);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $edwardsY
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $edwardsZ
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public static function edwards_to_montgomery(
        ParagonIE_Sodium_Core_Curve25519_Fe $edwardsY,
        ParagonIE_Sodium_Core_Curve25519_Fe $edwardsZ
    ) {
        $tempX = self::fe_add($edwardsZ, $edwardsY);
        $tempZ = self::fe_sub($edwardsZ, $edwardsY);
        $tempZ = self::fe_invert($tempZ);
        return self::fe_mul($tempX, $tempZ);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $n
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function crypto_scalarmult_curve25519_ref10_base($n)
    {
        # for (i = 0;i < 32;++i) e[i] = n[i];
        $e = '' . $n;

        # e[0] &= 248;
        $e[0] = self::intToChr(
            self::chrToInt($e[0]) & 248
        );

        # e[31] &= 127;
        # e[31] |= 64;
        $e[31] = self::intToChr(
            (self::chrToInt($e[31]) & 127) | 64
        );

        $A = self::ge_scalarmult_base($e);
        if (
            !($A->Y instanceof ParagonIE_Sodium_Core_Curve25519_Fe)
                ||
            !($A->Z instanceof ParagonIE_Sodium_Core_Curve25519_Fe)
        ) {
            throw new TypeError('Null points encountered');
        }
        $pk = self::edwards_to_montgomery($A->Y, $A->Z);
        return self::fe_tobytes($pk);
    }
}
Core/BLAKE2b.php000064400000056605152331075510007225 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_BLAKE2b', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_BLAKE2b
 *
 * Based on the work of Devi Mandiri in devi/salt.
 */
abstract class ParagonIE_Sodium_Core_BLAKE2b extends ParagonIE_Sodium_Core_Util
{
    /**
     * @var SplFixedArray
     */
    protected static $iv;

    /**
     * @var array<int, array<int, int>>
     */
    protected static $sigma = array(
        array(  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15),
        array( 14, 10,  4,  8,  9, 15, 13,  6,  1, 12,  0,  2, 11,  7,  5,  3),
        array( 11,  8, 12,  0,  5,  2, 15, 13, 10, 14,  3,  6,  7,  1,  9,  4),
        array(  7,  9,  3,  1, 13, 12, 11, 14,  2,  6,  5, 10,  4,  0, 15,  8),
        array(  9,  0,  5,  7,  2,  4, 10, 15, 14,  1, 11, 12,  6,  8,  3, 13),
        array(  2, 12,  6, 10,  0, 11,  8,  3,  4, 13,  7,  5, 15, 14,  1,  9),
        array( 12,  5,  1, 15, 14, 13,  4, 10,  0,  7,  6,  3,  9,  2,  8, 11),
        array( 13, 11,  7, 14, 12,  1,  3,  9,  5,  0, 15,  4,  8,  6,  2, 10),
        array(  6, 15, 14,  9, 11,  3,  0,  8, 12,  2, 13,  7,  1,  4, 10,  5),
        array( 10,  2,  8,  4,  7,  6,  1,  5, 15, 11,  9, 14,  3, 12, 13 , 0),
        array(  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15),
        array( 14, 10,  4,  8,  9, 15, 13,  6,  1, 12,  0,  2, 11,  7,  5,  3)
    );

    const BLOCKBYTES = 128;
    const OUTBYTES   = 64;
    const KEYBYTES   = 64;

    /**
     * Turn two 32-bit integers into a fixed array representing a 64-bit integer.
     *
     * @internal You should not use this directly from another application
     *
     * @param int $high
     * @param int $low
     * @return SplFixedArray
     * @psalm-suppress MixedAssignment
     */
    public static function new64($high, $low)
    {
        $i64 = new SplFixedArray(2);
        $i64[0] = $high & 0xffffffff;
        $i64[1] = $low & 0xffffffff;
        return $i64;
    }

    /**
     * Convert an arbitrary number into an SplFixedArray of two 32-bit integers
     * that represents a 64-bit integer.
     *
     * @internal You should not use this directly from another application
     *
     * @param int $num
     * @return SplFixedArray
     */
    protected static function to64($num)
    {
        list($hi, $lo) = self::numericTo64BitInteger($num);
        return self::new64($hi, $lo);
    }

    /**
     * Adds two 64-bit integers together, returning their sum as a SplFixedArray
     * containing two 32-bit integers (representing a 64-bit integer).
     *
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $x
     * @param SplFixedArray $y
     * @return SplFixedArray
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedOperand
     */
    protected static function add64($x, $y)
    {
        $l = ($x[1] + $y[1]) & 0xffffffff;
        return self::new64(
            (int) ($x[0] + $y[0] + (
                ($l < $x[1]) ? 1 : 0
            )),
            (int) $l
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $x
     * @param SplFixedArray $y
     * @param SplFixedArray $z
     * @return SplFixedArray
     */
    protected static function add364($x, $y, $z)
    {
        return self::add64($x, self::add64($y, $z));
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $x
     * @param SplFixedArray $y
     * @return SplFixedArray
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function xor64(SplFixedArray $x, SplFixedArray $y)
    {
        if (!is_numeric($x[0])) {
            throw new SodiumException('x[0] is not an integer');
        }
        if (!is_numeric($x[1])) {
            throw new SodiumException('x[1] is not an integer');
        }
        if (!is_numeric($y[0])) {
            throw new SodiumException('y[0] is not an integer');
        }
        if (!is_numeric($y[1])) {
            throw new SodiumException('y[1] is not an integer');
        }
        return self::new64(
            (int) (($x[0] ^ $y[0]) & 0xffffffff),
            (int) (($x[1] ^ $y[1]) & 0xffffffff)
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $x
     * @param int $c
     * @return SplFixedArray
     * @psalm-suppress MixedAssignment
     */
    public static function rotr64($x, $c)
    {
        if ($c >= 64) {
            $c %= 64;
        }
        if ($c >= 32) {
            /** @var int $tmp */
            $tmp = $x[0];
            $x[0] = $x[1];
            $x[1] = $tmp;
            $c -= 32;
        }
        if ($c === 0) {
            return $x;
        }

        $l0 = 0;
        $c = 64 - $c;

        if ($c < 32) {
            /** @var int $h0 */
            $h0 = ((int) ($x[0]) << $c) | (
                (
                    (int) ($x[1]) & ((1 << $c) - 1)
                        <<
                    (32 - $c)
                ) >> (32 - $c)
            );
            /** @var int $l0 */
            $l0 = (int) ($x[1]) << $c;
        } else {
            /** @var int $h0 */
            $h0 = (int) ($x[1]) << ($c - 32);
        }

        $h1 = 0;
        $c1 = 64 - $c;

        if ($c1 < 32) {
            /** @var int $h1 */
            $h1 = (int) ($x[0]) >> $c1;
            /** @var int $l1 */
            $l1 = ((int) ($x[1]) >> $c1) | ((int) ($x[0]) & ((1 << $c1) - 1)) << (32 - $c1);
        } else {
            /** @var int $l1 */
            $l1 = (int) ($x[0]) >> ($c1 - 32);
        }

        return self::new64($h0 | $h1, $l0 | $l1);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $x
     * @return int
     * @psalm-suppress MixedOperand
     */
    protected static function flatten64($x)
    {
        return (int) ($x[0] * 4294967296 + $x[1]);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $x
     * @param int $i
     * @return SplFixedArray
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedArrayOffset
     */
    protected static function load64(SplFixedArray $x, $i)
    {
        /** @var int $l */
        $l = (int) ($x[$i])
             | ((int) ($x[$i+1]) << 8)
             | ((int) ($x[$i+2]) << 16)
             | ((int) ($x[$i+3]) << 24);
        /** @var int $h */
        $h = (int) ($x[$i+4])
             | ((int) ($x[$i+5]) << 8)
             | ((int) ($x[$i+6]) << 16)
             | ((int) ($x[$i+7]) << 24);
        return self::new64($h, $l);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $x
     * @param int $i
     * @param SplFixedArray $u
     * @return void
     * @psalm-suppress MixedAssignment
     */
    protected static function store64(SplFixedArray $x, $i, SplFixedArray $u)
    {
        $maxLength = $x->getSize() - 1;
        for ($j = 0; $j < 8; ++$j) {
            /*
               [0, 1, 2, 3, 4, 5, 6, 7]
                    ... becomes ...
               [0, 0, 0, 0, 1, 1, 1, 1]
            */
            /** @var int $uIdx */
            $uIdx = ((7 - $j) & 4) >> 2;
            $x[$i]   = ((int) ($u[$uIdx]) & 0xff);
            if (++$i > $maxLength) {
                return;
            }
            /** @psalm-suppress MixedOperand */
            $u[$uIdx] >>= 8;
        }
    }

    /**
     * This just sets the $iv static variable.
     *
     * @internal You should not use this directly from another application
     *
     * @return void
     */
    public static function pseudoConstructor()
    {
        static $called = false;
        if ($called) {
            return;
        }
        self::$iv = new SplFixedArray(8);
        self::$iv[0] = self::new64(0x6a09e667, 0xf3bcc908);
        self::$iv[1] = self::new64(0xbb67ae85, 0x84caa73b);
        self::$iv[2] = self::new64(0x3c6ef372, 0xfe94f82b);
        self::$iv[3] = self::new64(0xa54ff53a, 0x5f1d36f1);
        self::$iv[4] = self::new64(0x510e527f, 0xade682d1);
        self::$iv[5] = self::new64(0x9b05688c, 0x2b3e6c1f);
        self::$iv[6] = self::new64(0x1f83d9ab, 0xfb41bd6b);
        self::$iv[7] = self::new64(0x5be0cd19, 0x137e2179);

        $called = true;
    }

    /**
     * Returns a fresh BLAKE2 context.
     *
     * @internal You should not use this directly from another application
     *
     * @return SplFixedArray
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     */
    protected static function context()
    {
        $ctx    = new SplFixedArray(6);
        $ctx[0] = new SplFixedArray(8);   // h
        $ctx[1] = new SplFixedArray(2);   // t
        $ctx[2] = new SplFixedArray(2);   // f
        $ctx[3] = new SplFixedArray(256); // buf
        $ctx[4] = 0;                      // buflen
        $ctx[5] = 0;                      // last_node (uint8_t)

        for ($i = 8; $i--;) {
            $ctx[0][$i] = self::$iv[$i];
        }
        for ($i = 256; $i--;) {
            $ctx[3][$i] = 0;
        }

        $zero = self::new64(0, 0);
        $ctx[1][0] = $zero;
        $ctx[1][1] = $zero;
        $ctx[2][0] = $zero;
        $ctx[2][1] = $zero;

        return $ctx;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $ctx
     * @param SplFixedArray $buf
     * @return void
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     * @psalm-suppress MixedArrayOffset
     */
    protected static function compress(SplFixedArray $ctx, SplFixedArray $buf)
    {
        $m = new SplFixedArray(16);
        $v = new SplFixedArray(16);

        for ($i = 16; $i--;) {
            $m[$i] = self::load64($buf, $i << 3);
        }

        for ($i = 8; $i--;) {
            $v[$i] = $ctx[0][$i];
        }

        $v[ 8] = self::$iv[0];
        $v[ 9] = self::$iv[1];
        $v[10] = self::$iv[2];
        $v[11] = self::$iv[3];

        $v[12] = self::xor64($ctx[1][0], self::$iv[4]);
        $v[13] = self::xor64($ctx[1][1], self::$iv[5]);
        $v[14] = self::xor64($ctx[2][0], self::$iv[6]);
        $v[15] = self::xor64($ctx[2][1], self::$iv[7]);

        for ($r = 0; $r < 12; ++$r) {
            $v = self::G($r, 0, 0, 4, 8, 12, $v, $m);
            $v = self::G($r, 1, 1, 5, 9, 13, $v, $m);
            $v = self::G($r, 2, 2, 6, 10, 14, $v, $m);
            $v = self::G($r, 3, 3, 7, 11, 15, $v, $m);
            $v = self::G($r, 4, 0, 5, 10, 15, $v, $m);
            $v = self::G($r, 5, 1, 6, 11, 12, $v, $m);
            $v = self::G($r, 6, 2, 7, 8, 13, $v, $m);
            $v = self::G($r, 7, 3, 4, 9, 14, $v, $m);
        }

        for ($i = 8; $i--;) {
            $ctx[0][$i] = self::xor64(
                $ctx[0][$i], self::xor64($v[$i], $v[$i+8])
            );
        }
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $r
     * @param int $i
     * @param int $a
     * @param int $b
     * @param int $c
     * @param int $d
     * @param SplFixedArray $v
     * @param SplFixedArray $m
     * @return SplFixedArray
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedArrayOffset
     */
    public static function G($r, $i, $a, $b, $c, $d, SplFixedArray $v, SplFixedArray $m)
    {
        $v[$a] = self::add364($v[$a], $v[$b], $m[self::$sigma[$r][$i << 1]]);
        $v[$d] = self::rotr64(self::xor64($v[$d], $v[$a]), 32);
        $v[$c] = self::add64($v[$c], $v[$d]);
        $v[$b] = self::rotr64(self::xor64($v[$b], $v[$c]), 24);
        $v[$a] = self::add364($v[$a], $v[$b], $m[self::$sigma[$r][($i << 1) + 1]]);
        $v[$d] = self::rotr64(self::xor64($v[$d], $v[$a]), 16);
        $v[$c] = self::add64($v[$c], $v[$d]);
        $v[$b] = self::rotr64(self::xor64($v[$b], $v[$c]), 63);
        return $v;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $ctx
     * @param int $inc
     * @return void
     * @throws SodiumException
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     */
    public static function increment_counter($ctx, $inc)
    {
        if ($inc < 0) {
            throw new SodiumException('Increasing by a negative number makes no sense.');
        }
        $t = self::to64($inc);
        # S->t is $ctx[1] in our implementation

        # S->t[0] = ( uint64_t )( t >> 0 );
        $ctx[1][0] = self::add64($ctx[1][0], $t);

        # S->t[1] += ( S->t[0] < inc );
        if (self::flatten64($ctx[1][0]) < $inc) {
            $ctx[1][1] = self::add64($ctx[1][1], self::to64(1));
        }
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $ctx
     * @param SplFixedArray $p
     * @param int $plen
     * @return void
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     * @psalm-suppress MixedArrayOffset
     * @psalm-suppress MixedOperand
     */
    public static function update(SplFixedArray $ctx, SplFixedArray $p, $plen)
    {
        self::pseudoConstructor();

        $offset = 0;
        while ($plen > 0) {
            $left = $ctx[4];
            $fill = 256 - $left;

            if ($plen > $fill) {
                # memcpy( S->buf + left, in, fill ); /* Fill buffer */
                for ($i = $fill; $i--;) {
                    $ctx[3][$i + $left] = $p[$i + $offset];
                }

                # S->buflen += fill;
                $ctx[4] += $fill;

                # blake2b_increment_counter( S, BLAKE2B_BLOCKBYTES );
                self::increment_counter($ctx, 128);

                # blake2b_compress( S, S->buf ); /* Compress */
                self::compress($ctx, $ctx[3]);

                # memcpy( S->buf, S->buf + BLAKE2B_BLOCKBYTES, BLAKE2B_BLOCKBYTES ); /* Shift buffer left */
                for ($i = 128; $i--;) {
                    $ctx[3][$i] = $ctx[3][$i + 128];
                }

                # S->buflen -= BLAKE2B_BLOCKBYTES;
                $ctx[4] -= 128;

                # in += fill;
                $offset += $fill;

                # inlen -= fill;
                $plen -= $fill;
            } else {
                for ($i = $plen; $i--;) {
                    $ctx[3][$i + $left] = $p[$i + $offset];
                }
                $ctx[4] += $plen;
                $offset += $plen;
                $plen -= $plen;
            }
        }
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $ctx
     * @param SplFixedArray $out
     * @return SplFixedArray
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     * @psalm-suppress MixedArrayOffset
     * @psalm-suppress MixedOperand
     */
    public static function finish(SplFixedArray $ctx, SplFixedArray $out)
    {
        self::pseudoConstructor();
        if ($ctx[4] > 128) {
            self::increment_counter($ctx, 128);
            self::compress($ctx, $ctx[3]);
            $ctx[4] -= 128;
            if ($ctx[4] > 128) {
                throw new SodiumException('Failed to assert that buflen <= 128 bytes');
            }
            for ($i = $ctx[4]; $i--;) {
                $ctx[3][$i] = $ctx[3][$i + 128];
            }
        }

        self::increment_counter($ctx, $ctx[4]);
        $ctx[2][0] = self::new64(0xffffffff, 0xffffffff);

        for ($i = 256 - $ctx[4]; $i--;) {
            $ctx[3][$i+$ctx[4]] = 0;
        }

        self::compress($ctx, $ctx[3]);

        $i = (int) (($out->getSize() - 1) / 8);
        for (; $i >= 0; --$i) {
            self::store64($out, $i << 3, $ctx[0][$i]);
        }
        return $out;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray|null $key
     * @param int $outlen
     * @param SplFixedArray|null $salt
     * @param SplFixedArray|null $personal
     * @return SplFixedArray
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     * @psalm-suppress MixedArrayOffset
     */
    public static function init(
        $key = null,
        $outlen = 64,
        $salt = null,
        $personal = null
    ) {
        self::pseudoConstructor();
        $klen = 0;

        if ($key !== null) {
            if (count($key) > 64) {
                throw new SodiumException('Invalid key size');
            }
            $klen = count($key);
        }

        if ($outlen > 64) {
            throw new SodiumException('Invalid output size');
        }

        $ctx = self::context();

        $p = new SplFixedArray(64);
        // Zero our param buffer...
        for ($i = 64; --$i;) {
            $p[$i] = 0;
        }

        $p[0] = $outlen; // digest_length
        $p[1] = $klen;   // key_length
        $p[2] = 1;       // fanout
        $p[3] = 1;       // depth

        if ($salt instanceof SplFixedArray) {
            // salt: [32] through [47]
            for ($i = 0; $i < 16; ++$i) {
                $p[32 + $i] = (int) $salt[$i];
            }
        }
        if ($personal instanceof SplFixedArray) {
            // personal: [48] through [63]
            for ($i = 0; $i < 16; ++$i) {
                $p[48 + $i] = (int) $personal[$i];
            }
        }

        $ctx[0][0] = self::xor64(
            $ctx[0][0],
            self::load64($p, 0)
        );
        if ($salt instanceof SplFixedArray || $personal instanceof SplFixedArray) {
            // We need to do what blake2b_init_param() does:
            for ($i = 1; $i < 8; ++$i) {
                $ctx[0][$i] = self::xor64(
                    $ctx[0][$i],
                    self::load64($p, $i << 3)
                );
            }
        }

        if ($klen > 0 && $key instanceof SplFixedArray) {
            $block = new SplFixedArray(128);
            for ($i = 128; $i--;) {
                $block[$i] = 0;
            }
            for ($i = $klen; $i--;) {
                $block[$i] = $key[$i];
            }
            self::update($ctx, $block, 128);
            $ctx[4] = 128;
        }

        return $ctx;
    }

    /**
     * Convert a string into an SplFixedArray of integers
     *
     * @internal You should not use this directly from another application
     *
     * @param string $str
     * @return SplFixedArray
     * @psalm-suppress MixedArgumentTypeCoercion
     */
    public static function stringToSplFixedArray($str = '')
    {
        $values = unpack('C*', $str);
        return SplFixedArray::fromArray(array_values($values));
    }

    /**
     * Convert an SplFixedArray of integers into a string
     *
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $a
     * @return string
     * @throws TypeError
     */
    public static function SplFixedArrayToString(SplFixedArray $a)
    {
        /**
         * @var array<int, int|string> $arr
         */
        $arr = $a->toArray();
        $c = $a->count();
        array_unshift($arr, str_repeat('C', $c));
        return (string) (call_user_func_array('pack', $arr));
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $ctx
     * @return string
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     * @psalm-suppress MixedArrayOffset
     * @psalm-suppress MixedMethodCall
     */
    public static function contextToString(SplFixedArray $ctx)
    {
        $str = '';
        /** @var array<int, array<int, int>> $ctxA */
        $ctxA = $ctx[0]->toArray();

        # uint64_t h[8];
        for ($i = 0; $i < 8; ++$i) {
            $str .= self::store32_le($ctxA[$i][1]);
            $str .= self::store32_le($ctxA[$i][0]);
        }

        # uint64_t t[2];
        # uint64_t f[2];
        for ($i = 1; $i < 3; ++$i) {
            $ctxA = $ctx[$i]->toArray();
            $str .= self::store32_le($ctxA[0][1]);
            $str .= self::store32_le($ctxA[0][0]);
            $str .= self::store32_le($ctxA[1][1]);
            $str .= self::store32_le($ctxA[1][0]);
        }

        # uint8_t buf[2 * 128];
        $str .= self::SplFixedArrayToString($ctx[3]);

        /** @var int $ctx4 */
        $ctx4 = (int) $ctx[4];

        # size_t buflen;
        $str .= implode('', array(
            self::intToChr($ctx4 & 0xff),
            self::intToChr(($ctx4 >> 8) & 0xff),
            self::intToChr(($ctx4 >> 16) & 0xff),
            self::intToChr(($ctx4 >> 24) & 0xff),
            self::intToChr(($ctx4 >> 32) & 0xff),
            self::intToChr(($ctx4 >> 40) & 0xff),
            self::intToChr(($ctx4 >> 48) & 0xff),
            self::intToChr(($ctx4 >> 56) & 0xff)
        ));
        # uint8_t last_node;
        return $str . self::intToChr($ctx[5]) . str_repeat("\x00", 23);
    }

    /**
     * Creates an SplFixedArray containing other SplFixedArray elements, from
     * a string (compatible with \Sodium\crypto_generichash_{init, update, final})
     *
     * @internal You should not use this directly from another application
     *
     * @param string $string
     * @return SplFixedArray
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArrayAssignment
     */
    public static function stringToContext($string)
    {
        $ctx = self::context();

        # uint64_t h[8];
        for ($i = 0; $i < 8; ++$i) {
            $ctx[0][$i] = SplFixedArray::fromArray(
                array(
                    self::load_4(
                        self::substr($string, (($i << 3) + 4), 4)
                    ),
                    self::load_4(
                        self::substr($string, (($i << 3) + 0), 4)
                    )
                )
            );
        }

        # uint64_t t[2];
        # uint64_t f[2];
        for ($i = 1; $i < 3; ++$i) {
            $ctx[$i][1] = SplFixedArray::fromArray(
                array(
                    self::load_4(self::substr($string, 76 + (($i - 1) << 4), 4)),
                    self::load_4(self::substr($string, 72 + (($i - 1) << 4), 4))
                )
            );
            $ctx[$i][0] = SplFixedArray::fromArray(
                array(
                    self::load_4(self::substr($string, 68 + (($i - 1) << 4), 4)),
                    self::load_4(self::substr($string, 64 + (($i - 1) << 4), 4))
                )
            );
        }

        # uint8_t buf[2 * 128];
        $ctx[3] = self::stringToSplFixedArray(self::substr($string, 96, 256));

        # uint8_t buf[2 * 128];
        $int = 0;
        for ($i = 0; $i < 8; ++$i) {
            $int |= self::chrToInt($string[352 + $i]) << ($i << 3);
        }
        $ctx[4] = $int;

        return $ctx;
    }
}
Core/Salsa20.php000064400000020051152331075510007352 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_Salsa20', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_Salsa20
 */
abstract class ParagonIE_Sodium_Core_Salsa20 extends ParagonIE_Sodium_Core_Util
{
    const ROUNDS = 20;

    /**
     * Calculate an salsa20 hash of a single block
     *
     * @internal You should not use this directly from another application
     *
     * @param string $in
     * @param string $k
     * @param string|null $c
     * @return string
     * @throws TypeError
     */
    public static function core_salsa20($in, $k, $c = null)
    {
        if (self::strlen($k) < 32) {
            throw new RangeException('Key must be 32 bytes long');
        }
        if ($c === null) {
            $j0  = $x0  = 0x61707865;
            $j5  = $x5  = 0x3320646e;
            $j10 = $x10 = 0x79622d32;
            $j15 = $x15 = 0x6b206574;
        } else {
            $j0  = $x0  = self::load_4(self::substr($c, 0, 4));
            $j5  = $x5  = self::load_4(self::substr($c, 4, 4));
            $j10 = $x10 = self::load_4(self::substr($c, 8, 4));
            $j15 = $x15 = self::load_4(self::substr($c, 12, 4));
        }
        $j1  = $x1  = self::load_4(self::substr($k, 0, 4));
        $j2  = $x2  = self::load_4(self::substr($k, 4, 4));
        $j3  = $x3  = self::load_4(self::substr($k, 8, 4));
        $j4  = $x4  = self::load_4(self::substr($k, 12, 4));
        $j6  = $x6  = self::load_4(self::substr($in, 0, 4));
        $j7  = $x7  = self::load_4(self::substr($in, 4, 4));
        $j8  = $x8  = self::load_4(self::substr($in, 8, 4));
        $j9  = $x9  = self::load_4(self::substr($in, 12, 4));
        $j11 = $x11 = self::load_4(self::substr($k, 16, 4));
        $j12 = $x12 = self::load_4(self::substr($k, 20, 4));
        $j13 = $x13 = self::load_4(self::substr($k, 24, 4));
        $j14 = $x14 = self::load_4(self::substr($k, 28, 4));

        for ($i = self::ROUNDS; $i > 0; $i -= 2) {
            $x4 ^= self::rotate($x0 + $x12, 7);
            $x8 ^= self::rotate($x4 + $x0, 9);
            $x12 ^= self::rotate($x8 + $x4, 13);
            $x0 ^= self::rotate($x12 + $x8, 18);

            $x9 ^= self::rotate($x5 + $x1, 7);
            $x13 ^= self::rotate($x9 + $x5, 9);
            $x1 ^= self::rotate($x13 + $x9, 13);
            $x5 ^= self::rotate($x1 + $x13, 18);

            $x14 ^= self::rotate($x10 + $x6, 7);
            $x2 ^= self::rotate($x14 + $x10, 9);
            $x6 ^= self::rotate($x2 + $x14, 13);
            $x10 ^= self::rotate($x6 + $x2, 18);

            $x3 ^= self::rotate($x15 + $x11, 7);
            $x7 ^= self::rotate($x3 + $x15, 9);
            $x11 ^= self::rotate($x7 + $x3, 13);
            $x15 ^= self::rotate($x11 + $x7, 18);

            $x1 ^= self::rotate($x0 + $x3, 7);
            $x2 ^= self::rotate($x1 + $x0, 9);
            $x3 ^= self::rotate($x2 + $x1, 13);
            $x0 ^= self::rotate($x3 + $x2, 18);

            $x6 ^= self::rotate($x5 + $x4, 7);
            $x7 ^= self::rotate($x6 + $x5, 9);
            $x4 ^= self::rotate($x7 + $x6, 13);
            $x5 ^= self::rotate($x4 + $x7, 18);

            $x11 ^= self::rotate($x10 + $x9, 7);
            $x8 ^= self::rotate($x11 + $x10, 9);
            $x9 ^= self::rotate($x8 + $x11, 13);
            $x10 ^= self::rotate($x9 + $x8, 18);

            $x12 ^= self::rotate($x15 + $x14, 7);
            $x13 ^= self::rotate($x12 + $x15, 9);
            $x14 ^= self::rotate($x13 + $x12, 13);
            $x15 ^= self::rotate($x14 + $x13, 18);
        }

        $x0  += $j0;
        $x1  += $j1;
        $x2  += $j2;
        $x3  += $j3;
        $x4  += $j4;
        $x5  += $j5;
        $x6  += $j6;
        $x7  += $j7;
        $x8  += $j8;
        $x9  += $j9;
        $x10 += $j10;
        $x11 += $j11;
        $x12 += $j12;
        $x13 += $j13;
        $x14 += $j14;
        $x15 += $j15;

        return self::store32_le($x0) .
            self::store32_le($x1) .
            self::store32_le($x2) .
            self::store32_le($x3) .
            self::store32_le($x4) .
            self::store32_le($x5) .
            self::store32_le($x6) .
            self::store32_le($x7) .
            self::store32_le($x8) .
            self::store32_le($x9) .
            self::store32_le($x10) .
            self::store32_le($x11) .
            self::store32_le($x12) .
            self::store32_le($x13) .
            self::store32_le($x14) .
            self::store32_le($x15);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $len
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function salsa20($len, $nonce, $key)
    {
        if (self::strlen($key) !== 32) {
            throw new RangeException('Key must be 32 bytes long');
        }
        $kcopy = '' . $key;
        $in = self::substr($nonce, 0, 8) . str_repeat("\0", 8);
        $c = '';
        while ($len >= 64) {
            $c .= self::core_salsa20($in, $kcopy, null);
            $u = 1;
            // Internal counter.
            for ($i = 8; $i < 16; ++$i) {
                $u += self::chrToInt($in[$i]);
                $in[$i] = self::intToChr($u & 0xff);
                $u >>= 8;
            }
            $len -= 64;
        }
        if ($len > 0) {
            $c .= self::substr(
                self::core_salsa20($in, $kcopy, null),
                0,
                $len
            );
        }
        try {
            ParagonIE_Sodium_Compat::memzero($kcopy);
        } catch (SodiumException $ex) {
            $kcopy = null;
        }
        return $c;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $m
     * @param string $n
     * @param int $ic
     * @param string $k
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function salsa20_xor_ic($m, $n, $ic, $k)
    {
        $mlen = self::strlen($m);
        if ($mlen < 1) {
            return '';
        }
        $kcopy = self::substr($k, 0, 32);
        $in = self::substr($n, 0, 8);
        // Initialize the counter
        $in .= ParagonIE_Sodium_Core_Util::store64_le($ic);

        $c = '';
        while ($mlen >= 64) {
            $block = self::core_salsa20($in, $kcopy, null);
            $c .= self::xorStrings(
                self::substr($m, 0, 64),
                self::substr($block, 0, 64)
            );
            $u = 1;
            for ($i = 8; $i < 16; ++$i) {
                $u += self::chrToInt($in[$i]);
                $in[$i] = self::intToChr($u & 0xff);
                $u >>= 8;
            }

            $mlen -= 64;
            $m = self::substr($m, 64);
        }

        if ($mlen) {
            $block = self::core_salsa20($in, $kcopy, null);
            $c .= self::xorStrings(
                self::substr($m, 0, $mlen),
                self::substr($block, 0, $mlen)
            );
        }
        try {
            ParagonIE_Sodium_Compat::memzero($block);
            ParagonIE_Sodium_Compat::memzero($kcopy);
        } catch (SodiumException $ex) {
            $block = null;
            $kcopy = null;
        }

        return $c;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function salsa20_xor($message, $nonce, $key)
    {
        return self::xorStrings(
            $message,
            self::salsa20(
                self::strlen($message),
                $nonce,
                $key
            )
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $u
     * @param int $c
     * @return int
     */
    public static function rotate($u, $c)
    {
        $u &= 0xffffffff;
        $c %= 32;
        return (int) (0xffffffff & (
                ($u << $c)
                    |
                ($u >> (32 - $c))
            )
        );
    }
}
Core/ChaCha20/IetfCtx.php000064400000002452152331075510010771 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_ChaCha20_IetfCtx', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_ChaCha20_IetfCtx
 */
class ParagonIE_Sodium_Core_ChaCha20_IetfCtx extends ParagonIE_Sodium_Core_ChaCha20_Ctx
{
    /**
     * ParagonIE_Sodium_Core_ChaCha20_IetfCtx constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $key     ChaCha20 key.
     * @param string $iv      Initialization Vector (a.k.a. nonce).
     * @param string $counter The initial counter value.
     *                        Defaults to 4 0x00 bytes.
     * @throws InvalidArgumentException
     * @throws TypeError
     */
    public function __construct($key = '', $iv = '', $counter = '')
    {
        if (self::strlen($iv) !== 12) {
            throw new InvalidArgumentException('ChaCha20 expects a 96-bit nonce in IETF mode.');
        }
        parent::__construct($key, self::substr($iv, 0, 8), $counter);

        if (!empty($counter)) {
            $this->container[12] = self::load_4(self::substr($counter, 0, 4));
        }
        $this->container[13] = self::load_4(self::substr($iv, 0, 4));
        $this->container[14] = self::load_4(self::substr($iv, 4, 4));
        $this->container[15] = self::load_4(self::substr($iv, 8, 4));
    }
}
Core/ChaCha20/Ctx.php000064400000007366152331075510010172 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_ChaCha20_Ctx', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_ChaCha20_Ctx
 */
class ParagonIE_Sodium_Core_ChaCha20_Ctx extends ParagonIE_Sodium_Core_Util implements ArrayAccess
{
    /**
     * @var SplFixedArray internally, <int, int>
     */
    protected $container;

    /**
     * ParagonIE_Sodium_Core_ChaCha20_Ctx constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $key     ChaCha20 key.
     * @param string $iv      Initialization Vector (a.k.a. nonce).
     * @param string $counter The initial counter value.
     *                        Defaults to 8 0x00 bytes.
     * @throws InvalidArgumentException
     * @throws TypeError
     */
    public function __construct($key = '', $iv = '', $counter = '')
    {
        if (self::strlen($key) !== 32) {
            throw new InvalidArgumentException('ChaCha20 expects a 256-bit key.');
        }
        if (self::strlen($iv) !== 8) {
            throw new InvalidArgumentException('ChaCha20 expects a 64-bit nonce.');
        }
        $this->container = new SplFixedArray(16);

        /* "expand 32-byte k" as per ChaCha20 spec */
        $this->container[0]  = 0x61707865;
        $this->container[1]  = 0x3320646e;
        $this->container[2]  = 0x79622d32;
        $this->container[3]  = 0x6b206574;
        $this->container[4]  = self::load_4(self::substr($key, 0, 4));
        $this->container[5]  = self::load_4(self::substr($key, 4, 4));
        $this->container[6]  = self::load_4(self::substr($key, 8, 4));
        $this->container[7]  = self::load_4(self::substr($key, 12, 4));
        $this->container[8]  = self::load_4(self::substr($key, 16, 4));
        $this->container[9]  = self::load_4(self::substr($key, 20, 4));
        $this->container[10] = self::load_4(self::substr($key, 24, 4));
        $this->container[11] = self::load_4(self::substr($key, 28, 4));

        if (empty($counter)) {
            $this->container[12] = 0;
            $this->container[13] = 0;
        } else {
            $this->container[12] = self::load_4(self::substr($counter, 0, 4));
            $this->container[13] = self::load_4(self::substr($counter, 4, 4));
        }
        $this->container[14] = self::load_4(self::substr($iv, 0, 4));
        $this->container[15] = self::load_4(self::substr($iv, 4, 4));
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $offset
     * @param int $value
     * @return void
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetSet($offset, $value)
    {
        if (!is_int($offset)) {
            throw new InvalidArgumentException('Expected an integer');
        }
        if (!is_int($value)) {
            throw new InvalidArgumentException('Expected an integer');
        }
        $this->container[$offset] = $value;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $offset
     * @return bool
     */
    public function offsetExists($offset)
    {
        return isset($this->container[$offset]);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $offset
     * @return void
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetUnset($offset)
    {
        unset($this->container[$offset]);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $offset
     * @return mixed|null
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetGet($offset)
    {
        return isset($this->container[$offset])
            ? $this->container[$offset]
            : null;
    }
}
Core/HChaCha20.php000064400000007437152331075510007543 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_HChaCha20', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_HChaCha20
 */
class ParagonIE_Sodium_Core_HChaCha20 extends ParagonIE_Sodium_Core_ChaCha20
{
    /**
     * @param string $in
     * @param string $key
     * @param string|null $c
     * @return string
     * @throws TypeError
     */
    public static function hChaCha20($in = '', $key = '', $c = null)
    {
        $ctx = array();

        if ($c === null) {
            $ctx[0] = 0x61707865;
            $ctx[1] = 0x3320646e;
            $ctx[2] = 0x79622d32;
            $ctx[3] = 0x6b206574;
        } else {
            $ctx[0] = self::load_4(self::substr($c,  0, 4));
            $ctx[1] = self::load_4(self::substr($c,  4, 4));
            $ctx[2] = self::load_4(self::substr($c,  8, 4));
            $ctx[3] = self::load_4(self::substr($c, 12, 4));
        }
        $ctx[4]  = self::load_4(self::substr($key,  0, 4));
        $ctx[5]  = self::load_4(self::substr($key,  4, 4));
        $ctx[6]  = self::load_4(self::substr($key,  8, 4));
        $ctx[7]  = self::load_4(self::substr($key, 12, 4));
        $ctx[8]  = self::load_4(self::substr($key, 16, 4));
        $ctx[9]  = self::load_4(self::substr($key, 20, 4));
        $ctx[10] = self::load_4(self::substr($key, 24, 4));
        $ctx[11] = self::load_4(self::substr($key, 28, 4));
        $ctx[12] = self::load_4(self::substr($in,   0, 4));
        $ctx[13] = self::load_4(self::substr($in,   4, 4));
        $ctx[14] = self::load_4(self::substr($in,   8, 4));
        $ctx[15] = self::load_4(self::substr($in,  12, 4));
        return self::hChaCha20Bytes($ctx);
    }

    /**
     * @param array $ctx
     * @return string
     * @throws TypeError
     */
    protected static function hChaCha20Bytes(array $ctx)
    {
        $x0  = (int) $ctx[0];
        $x1  = (int) $ctx[1];
        $x2  = (int) $ctx[2];
        $x3  = (int) $ctx[3];
        $x4  = (int) $ctx[4];
        $x5  = (int) $ctx[5];
        $x6  = (int) $ctx[6];
        $x7  = (int) $ctx[7];
        $x8  = (int) $ctx[8];
        $x9  = (int) $ctx[9];
        $x10 = (int) $ctx[10];
        $x11 = (int) $ctx[11];
        $x12 = (int) $ctx[12];
        $x13 = (int) $ctx[13];
        $x14 = (int) $ctx[14];
        $x15 = (int) $ctx[15];

        for ($i = 0; $i < 10; ++$i) {
            # QUARTERROUND( x0,  x4,  x8,  x12)
            list($x0, $x4, $x8, $x12) = self::quarterRound($x0, $x4, $x8, $x12);

            # QUARTERROUND( x1,  x5,  x9,  x13)
            list($x1, $x5, $x9, $x13) = self::quarterRound($x1, $x5, $x9, $x13);

            # QUARTERROUND( x2,  x6,  x10,  x14)
            list($x2, $x6, $x10, $x14) = self::quarterRound($x2, $x6, $x10, $x14);

            # QUARTERROUND( x3,  x7,  x11,  x15)
            list($x3, $x7, $x11, $x15) = self::quarterRound($x3, $x7, $x11, $x15);

            # QUARTERROUND( x0,  x5,  x10,  x15)
            list($x0, $x5, $x10, $x15) = self::quarterRound($x0, $x5, $x10, $x15);

            # QUARTERROUND( x1,  x6,  x11,  x12)
            list($x1, $x6, $x11, $x12) = self::quarterRound($x1, $x6, $x11, $x12);

            # QUARTERROUND( x2,  x7,  x8,  x13)
            list($x2, $x7, $x8, $x13) = self::quarterRound($x2, $x7, $x8, $x13);

            # QUARTERROUND( x3,  x4,  x9,  x14)
            list($x3, $x4, $x9, $x14) = self::quarterRound($x3, $x4, $x9, $x14);
        }

        return self::store32_le((int) ($x0  & 0xffffffff)) .
            self::store32_le((int) ($x1  & 0xffffffff)) .
            self::store32_le((int) ($x2  & 0xffffffff)) .
            self::store32_le((int) ($x3  & 0xffffffff)) .
            self::store32_le((int) ($x12 & 0xffffffff)) .
            self::store32_le((int) ($x13 & 0xffffffff)) .
            self::store32_le((int) ($x14 & 0xffffffff)) .
            self::store32_le((int) ($x15 & 0xffffffff));
    }
}
Core/SipHash.php000064400000020051152331075510007504 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_SipHash', false)) {
    return;
}

/**
 * Class ParagonIE_SodiumCompat_Core_SipHash
 *
 * Only uses 32-bit arithmetic, while the original SipHash used 64-bit integers
 */
class ParagonIE_Sodium_Core_SipHash extends ParagonIE_Sodium_Core_Util
{
    /**
     * @internal You should not use this directly from another application
     *
     * @param int[] $v
     * @return int[]
     *
     */
    public static function sipRound(array $v)
    {
        # v0 += v1;
        list($v[0], $v[1]) = self::add(
            array($v[0], $v[1]),
            array($v[2], $v[3])
        );

        #  v1=ROTL(v1,13);
        list($v[2], $v[3]) = self::rotl_64((int) $v[2], (int) $v[3], 13);

        #  v1 ^= v0;
        $v[2] = (int) $v[2] ^ (int) $v[0];
        $v[3] = (int) $v[3] ^ (int) $v[1];

        #  v0=ROTL(v0,32);
        list($v[0], $v[1]) = self::rotl_64((int) $v[0], (int) $v[1], 32);

        # v2 += v3;
        list($v[4], $v[5]) = self::add(
            array((int) $v[4], (int) $v[5]),
            array((int) $v[6], (int) $v[7])
        );

        # v3=ROTL(v3,16);
        list($v[6], $v[7]) = self::rotl_64((int) $v[6], (int) $v[7], 16);

        #  v3 ^= v2;
        $v[6] = (int) $v[6] ^ (int) $v[4];
        $v[7] = (int) $v[7] ^ (int) $v[5];

        # v0 += v3;
        list($v[0], $v[1]) = self::add(
            array((int) $v[0], (int) $v[1]),
            array((int) $v[6], (int) $v[7])
        );

        # v3=ROTL(v3,21);
        list($v[6], $v[7]) = self::rotl_64((int) $v[6], (int) $v[7], 21);

        # v3 ^= v0;
        $v[6] = (int) $v[6] ^ (int) $v[0];
        $v[7] = (int) $v[7] ^ (int) $v[1];

        # v2 += v1;
        list($v[4], $v[5]) = self::add(
            array((int) $v[4], (int) $v[5]),
            array((int) $v[2], (int) $v[3])
        );

        # v1=ROTL(v1,17);
        list($v[2], $v[3]) = self::rotl_64((int) $v[2], (int) $v[3], 17);

        #  v1 ^= v2;;
        $v[2] = (int) $v[2] ^ (int) $v[4];
        $v[3] = (int) $v[3] ^ (int) $v[5];

        # v2=ROTL(v2,32)
        list($v[4], $v[5]) = self::rotl_64((int) $v[4], (int) $v[5], 32);

        return $v;
    }

    /**
     * Add two 32 bit integers representing a 64-bit integer.
     *
     * @internal You should not use this directly from another application
     *
     * @param int[] $a
     * @param int[] $b
     * @return array<int, mixed>
     */
    public static function add(array $a, array $b)
    {
        /** @var int $x1 */
        $x1 = $a[1] + $b[1];
        /** @var int $c */
        $c = $x1 >> 32; // Carry if ($a + $b) > 0xffffffff
        /** @var int $x0 */
        $x0 = $a[0] + $b[0] + $c;
        return array(
            $x0 & 0xffffffff,
            $x1 & 0xffffffff
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $int0
     * @param int $int1
     * @param int $c
     * @return array<int, mixed>
     */
    public static function rotl_64($int0, $int1, $c)
    {
        $int0 &= 0xffffffff;
        $int1 &= 0xffffffff;
        $c &= 63;
        if ($c === 32) {
            return array($int1, $int0);
        }
        if ($c > 31) {
            $tmp = $int1;
            $int1 = $int0;
            $int0 = $tmp;
            $c &= 31;
        }
        if ($c === 0) {
            return array($int0, $int1);
        }
        return array(
            0xffffffff & (
                ($int0 << $c)
                    |
                ($int1 >> (32 - $c))
            ),
            0xffffffff & (
                ($int1 << $c)
                    |
                ($int0 >> (32 - $c))
            ),
        );
    }

    /**
     * Implements Siphash-2-4 using only 32-bit numbers.
     *
     * When we split an int into two, the higher bits go to the lower index.
     * e.g. 0xDEADBEEFAB10C92D becomes [
     *     0 => 0xDEADBEEF,
     *     1 => 0xAB10C92D
     * ].
     *
     * @internal You should not use this directly from another application
     *
     * @param string $in
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sipHash24($in, $key)
    {
        $inlen = self::strlen($in);

        # /* "somepseudorandomlygeneratedbytes" */
        # u64 v0 = 0x736f6d6570736575ULL;
        # u64 v1 = 0x646f72616e646f6dULL;
        # u64 v2 = 0x6c7967656e657261ULL;
        # u64 v3 = 0x7465646279746573ULL;
        $v = array(
            0x736f6d65, // 0
            0x70736575, // 1
            0x646f7261, // 2
            0x6e646f6d, // 3
            0x6c796765, // 4
            0x6e657261, // 5
            0x74656462, // 6
            0x79746573  // 7
        );
        // v0 => $v[0], $v[1]
        // v1 => $v[2], $v[3]
        // v2 => $v[4], $v[5]
        // v3 => $v[6], $v[7]

        # u64 k0 = LOAD64_LE( k );
        # u64 k1 = LOAD64_LE( k + 8 );
        $k = array(
            self::load_4(self::substr($key, 4, 4)),
            self::load_4(self::substr($key, 0, 4)),
            self::load_4(self::substr($key, 12, 4)),
            self::load_4(self::substr($key, 8, 4))
        );
        // k0 => $k[0], $k[1]
        // k1 => $k[2], $k[3]

        # b = ( ( u64 )inlen ) << 56;
        $b = array(
            $inlen << 24,
            0
        );
        // See docblock for why the 0th index gets the higher bits.

        # v3 ^= k1;
        $v[6] ^= $k[2];
        $v[7] ^= $k[3];
        # v2 ^= k0;
        $v[4] ^= $k[0];
        $v[5] ^= $k[1];
        # v1 ^= k1;
        $v[2] ^= $k[2];
        $v[3] ^= $k[3];
        # v0 ^= k0;
        $v[0] ^= $k[0];
        $v[1] ^= $k[1];

        $left = $inlen;
        # for ( ; in != end; in += 8 )
        while ($left >= 8) {
            # m = LOAD64_LE( in );
            $m = array(
                self::load_4(self::substr($in, 4, 4)),
                self::load_4(self::substr($in, 0, 4))
            );

            # v3 ^= m;
            $v[6] ^= $m[0];
            $v[7] ^= $m[1];

            # SIPROUND;
            # SIPROUND;
            $v = self::sipRound($v);
            $v = self::sipRound($v);

            # v0 ^= m;
            $v[0] ^= $m[0];
            $v[1] ^= $m[1];

            $in = self::substr($in, 8);
            $left -= 8;
        }

        # switch( left )
        #  {
        #     case 7: b |= ( ( u64 )in[ 6] )  << 48;
        #     case 6: b |= ( ( u64 )in[ 5] )  << 40;
        #     case 5: b |= ( ( u64 )in[ 4] )  << 32;
        #     case 4: b |= ( ( u64 )in[ 3] )  << 24;
        #     case 3: b |= ( ( u64 )in[ 2] )  << 16;
        #     case 2: b |= ( ( u64 )in[ 1] )  <<  8;
        #     case 1: b |= ( ( u64 )in[ 0] ); break;
        #     case 0: break;
        # }
        switch ($left) {
            case 7:
                $b[0] |= self::chrToInt($in[6]) << 16;
            case 6:
                $b[0] |= self::chrToInt($in[5]) << 8;
            case 5:
                $b[0] |= self::chrToInt($in[4]);
            case 4:
                $b[1] |= self::chrToInt($in[3]) << 24;
            case 3:
                $b[1] |= self::chrToInt($in[2]) << 16;
            case 2:
                $b[1] |= self::chrToInt($in[1]) << 8;
            case 1:
                $b[1] |= self::chrToInt($in[0]);
            case 0:
                break;
        }
        // See docblock for why the 0th index gets the higher bits.

        # v3 ^= b;
        $v[6] ^= $b[0];
        $v[7] ^= $b[1];

        # SIPROUND;
        # SIPROUND;
        $v = self::sipRound($v);
        $v = self::sipRound($v);

        # v0 ^= b;
        $v[0] ^= $b[0];
        $v[1] ^= $b[1];

        // Flip the lower 8 bits of v2 which is ($v[4], $v[5]) in our implementation
        # v2 ^= 0xff;
        $v[5] ^= 0xff;

        # SIPROUND;
        # SIPROUND;
        # SIPROUND;
        # SIPROUND;
        $v = self::sipRound($v);
        $v = self::sipRound($v);
        $v = self::sipRound($v);
        $v = self::sipRound($v);

        # b = v0 ^ v1 ^ v2 ^ v3;
        # STORE64_LE( out, b );
        return  self::store32_le($v[1] ^ $v[3] ^ $v[5] ^ $v[7]) .
            self::store32_le($v[0] ^ $v[2] ^ $v[4] ^ $v[6]);
    }
}
Core/ChaCha20.php000064400000031206152331075510007422 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_ChaCha20', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_ChaCha20
 */
class ParagonIE_Sodium_Core_ChaCha20 extends ParagonIE_Sodium_Core_Util
{
    /**
     * Bitwise left rotation
     *
     * @internal You should not use this directly from another application
     *
     * @param int $v
     * @param int $n
     * @return int
     */
    public static function rotate($v, $n)
    {
        $v &= 0xffffffff;
        $n &= 31;
        return (int) (
            0xffffffff & (
                ($v << $n)
                    |
                ($v >> (32 - $n))
            )
        );
    }

    /**
     * The ChaCha20 quarter round function. Works on four 32-bit integers.
     *
     * @internal You should not use this directly from another application
     *
     * @param int $a
     * @param int $b
     * @param int $c
     * @param int $d
     * @return array<int, int>
     */
    protected static function quarterRound($a, $b, $c, $d)
    {
        # a = PLUS(a,b); d = ROTATE(XOR(d,a),16);
        /** @var int $a */
        $a = ($a + $b) & 0xffffffff;
        $d = self::rotate($d ^ $a, 16);

        # c = PLUS(c,d); b = ROTATE(XOR(b,c),12);
        /** @var int $c */
        $c = ($c + $d) & 0xffffffff;
        $b = self::rotate($b ^ $c, 12);

        # a = PLUS(a,b); d = ROTATE(XOR(d,a), 8);
        /** @var int $a */
        $a = ($a + $b) & 0xffffffff;
        $d = self::rotate($d ^ $a, 8);

        # c = PLUS(c,d); b = ROTATE(XOR(b,c), 7);
        /** @var int $c */
        $c = ($c + $d) & 0xffffffff;
        $b = self::rotate($b ^ $c, 7);
        return array((int) $a, (int) $b, (int) $c, (int) $d);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_ChaCha20_Ctx $ctx
     * @param string $message
     *
     * @return string
     * @throws TypeError
     * @throws SodiumException
     */
    public static function encryptBytes(
        ParagonIE_Sodium_Core_ChaCha20_Ctx $ctx,
        $message = ''
    ) {
        $bytes = self::strlen($message);

        /*
        j0 = ctx->input[0];
        j1 = ctx->input[1];
        j2 = ctx->input[2];
        j3 = ctx->input[3];
        j4 = ctx->input[4];
        j5 = ctx->input[5];
        j6 = ctx->input[6];
        j7 = ctx->input[7];
        j8 = ctx->input[8];
        j9 = ctx->input[9];
        j10 = ctx->input[10];
        j11 = ctx->input[11];
        j12 = ctx->input[12];
        j13 = ctx->input[13];
        j14 = ctx->input[14];
        j15 = ctx->input[15];
        */
        $j0  = (int) $ctx[0];
        $j1  = (int) $ctx[1];
        $j2  = (int) $ctx[2];
        $j3  = (int) $ctx[3];
        $j4  = (int) $ctx[4];
        $j5  = (int) $ctx[5];
        $j6  = (int) $ctx[6];
        $j7  = (int) $ctx[7];
        $j8  = (int) $ctx[8];
        $j9  = (int) $ctx[9];
        $j10 = (int) $ctx[10];
        $j11 = (int) $ctx[11];
        $j12 = (int) $ctx[12];
        $j13 = (int) $ctx[13];
        $j14 = (int) $ctx[14];
        $j15 = (int) $ctx[15];

        $c = '';
        for (;;) {
            if ($bytes < 64) {
                $message .= str_repeat("\x00", 64 - $bytes);
            }

            $x0 =  (int) $j0;
            $x1 =  (int) $j1;
            $x2 =  (int) $j2;
            $x3 =  (int) $j3;
            $x4 =  (int) $j4;
            $x5 =  (int) $j5;
            $x6 =  (int) $j6;
            $x7 =  (int) $j7;
            $x8 =  (int) $j8;
            $x9 =  (int) $j9;
            $x10 = (int) $j10;
            $x11 = (int) $j11;
            $x12 = (int) $j12;
            $x13 = (int) $j13;
            $x14 = (int) $j14;
            $x15 = (int) $j15;

            # for (i = 20; i > 0; i -= 2) {
            for ($i = 20; $i > 0; $i -= 2) {
                # QUARTERROUND( x0,  x4,  x8,  x12)
                list($x0, $x4, $x8, $x12) = self::quarterRound($x0, $x4, $x8, $x12);

                # QUARTERROUND( x1,  x5,  x9,  x13)
                list($x1, $x5, $x9, $x13) = self::quarterRound($x1, $x5, $x9, $x13);

                # QUARTERROUND( x2,  x6,  x10,  x14)
                list($x2, $x6, $x10, $x14) = self::quarterRound($x2, $x6, $x10, $x14);

                # QUARTERROUND( x3,  x7,  x11,  x15)
                list($x3, $x7, $x11, $x15) = self::quarterRound($x3, $x7, $x11, $x15);

                # QUARTERROUND( x0,  x5,  x10,  x15)
                list($x0, $x5, $x10, $x15) = self::quarterRound($x0, $x5, $x10, $x15);

                # QUARTERROUND( x1,  x6,  x11,  x12)
                list($x1, $x6, $x11, $x12) = self::quarterRound($x1, $x6, $x11, $x12);

                # QUARTERROUND( x2,  x7,  x8,  x13)
                list($x2, $x7, $x8, $x13) = self::quarterRound($x2, $x7, $x8, $x13);

                # QUARTERROUND( x3,  x4,  x9,  x14)
                list($x3, $x4, $x9, $x14) = self::quarterRound($x3, $x4, $x9, $x14);
            }
            /*
            x0 = PLUS(x0, j0);
            x1 = PLUS(x1, j1);
            x2 = PLUS(x2, j2);
            x3 = PLUS(x3, j3);
            x4 = PLUS(x4, j4);
            x5 = PLUS(x5, j5);
            x6 = PLUS(x6, j6);
            x7 = PLUS(x7, j7);
            x8 = PLUS(x8, j8);
            x9 = PLUS(x9, j9);
            x10 = PLUS(x10, j10);
            x11 = PLUS(x11, j11);
            x12 = PLUS(x12, j12);
            x13 = PLUS(x13, j13);
            x14 = PLUS(x14, j14);
            x15 = PLUS(x15, j15);
            */
            /** @var int $x0 */
            $x0  = ($x0 & 0xffffffff) + $j0;
            /** @var int $x1 */
            $x1  = ($x1 & 0xffffffff) + $j1;
            /** @var int $x2 */
            $x2  = ($x2 & 0xffffffff) + $j2;
            /** @var int $x3 */
            $x3  = ($x3 & 0xffffffff) + $j3;
            /** @var int $x4 */
            $x4  = ($x4 & 0xffffffff) + $j4;
            /** @var int $x5 */
            $x5  = ($x5 & 0xffffffff) + $j5;
            /** @var int $x6 */
            $x6  = ($x6 & 0xffffffff) + $j6;
            /** @var int $x7 */
            $x7  = ($x7 & 0xffffffff) + $j7;
            /** @var int $x8 */
            $x8  = ($x8 & 0xffffffff) + $j8;
            /** @var int $x9 */
            $x9  = ($x9 & 0xffffffff) + $j9;
            /** @var int $x10 */
            $x10 = ($x10 & 0xffffffff) + $j10;
            /** @var int $x11 */
            $x11 = ($x11 & 0xffffffff) + $j11;
            /** @var int $x12 */
            $x12 = ($x12 & 0xffffffff) + $j12;
            /** @var int $x13 */
            $x13 = ($x13 & 0xffffffff) + $j13;
            /** @var int $x14 */
            $x14 = ($x14 & 0xffffffff) + $j14;
            /** @var int $x15 */
            $x15 = ($x15 & 0xffffffff) + $j15;

            /*
            x0 = XOR(x0, LOAD32_LE(m + 0));
            x1 = XOR(x1, LOAD32_LE(m + 4));
            x2 = XOR(x2, LOAD32_LE(m + 8));
            x3 = XOR(x3, LOAD32_LE(m + 12));
            x4 = XOR(x4, LOAD32_LE(m + 16));
            x5 = XOR(x5, LOAD32_LE(m + 20));
            x6 = XOR(x6, LOAD32_LE(m + 24));
            x7 = XOR(x7, LOAD32_LE(m + 28));
            x8 = XOR(x8, LOAD32_LE(m + 32));
            x9 = XOR(x9, LOAD32_LE(m + 36));
            x10 = XOR(x10, LOAD32_LE(m + 40));
            x11 = XOR(x11, LOAD32_LE(m + 44));
            x12 = XOR(x12, LOAD32_LE(m + 48));
            x13 = XOR(x13, LOAD32_LE(m + 52));
            x14 = XOR(x14, LOAD32_LE(m + 56));
            x15 = XOR(x15, LOAD32_LE(m + 60));
            */
            $x0  ^= self::load_4(self::substr($message, 0, 4));
            $x1  ^= self::load_4(self::substr($message, 4, 4));
            $x2  ^= self::load_4(self::substr($message, 8, 4));
            $x3  ^= self::load_4(self::substr($message, 12, 4));
            $x4  ^= self::load_4(self::substr($message, 16, 4));
            $x5  ^= self::load_4(self::substr($message, 20, 4));
            $x6  ^= self::load_4(self::substr($message, 24, 4));
            $x7  ^= self::load_4(self::substr($message, 28, 4));
            $x8  ^= self::load_4(self::substr($message, 32, 4));
            $x9  ^= self::load_4(self::substr($message, 36, 4));
            $x10 ^= self::load_4(self::substr($message, 40, 4));
            $x11 ^= self::load_4(self::substr($message, 44, 4));
            $x12 ^= self::load_4(self::substr($message, 48, 4));
            $x13 ^= self::load_4(self::substr($message, 52, 4));
            $x14 ^= self::load_4(self::substr($message, 56, 4));
            $x15 ^= self::load_4(self::substr($message, 60, 4));

            /*
                j12 = PLUSONE(j12);
                if (!j12) {
                    j13 = PLUSONE(j13);
                }
             */
            ++$j12;
            if ($j12 & 0xf0000000) {
                throw new SodiumException('Overflow');
            }

            /*
            STORE32_LE(c + 0, x0);
            STORE32_LE(c + 4, x1);
            STORE32_LE(c + 8, x2);
            STORE32_LE(c + 12, x3);
            STORE32_LE(c + 16, x4);
            STORE32_LE(c + 20, x5);
            STORE32_LE(c + 24, x6);
            STORE32_LE(c + 28, x7);
            STORE32_LE(c + 32, x8);
            STORE32_LE(c + 36, x9);
            STORE32_LE(c + 40, x10);
            STORE32_LE(c + 44, x11);
            STORE32_LE(c + 48, x12);
            STORE32_LE(c + 52, x13);
            STORE32_LE(c + 56, x14);
            STORE32_LE(c + 60, x15);
            */
            $block = self::store32_le((int) ($x0  & 0xffffffff)) .
                 self::store32_le((int) ($x1  & 0xffffffff)) .
                 self::store32_le((int) ($x2  & 0xffffffff)) .
                 self::store32_le((int) ($x3  & 0xffffffff)) .
                 self::store32_le((int) ($x4  & 0xffffffff)) .
                 self::store32_le((int) ($x5  & 0xffffffff)) .
                 self::store32_le((int) ($x6  & 0xffffffff)) .
                 self::store32_le((int) ($x7  & 0xffffffff)) .
                 self::store32_le((int) ($x8  & 0xffffffff)) .
                 self::store32_le((int) ($x9  & 0xffffffff)) .
                 self::store32_le((int) ($x10 & 0xffffffff)) .
                 self::store32_le((int) ($x11 & 0xffffffff)) .
                 self::store32_le((int) ($x12 & 0xffffffff)) .
                 self::store32_le((int) ($x13 & 0xffffffff)) .
                 self::store32_le((int) ($x14 & 0xffffffff)) .
                 self::store32_le((int) ($x15 & 0xffffffff));

            /* Partial block */
            if ($bytes < 64) {
                $c .= self::substr($block, 0, $bytes);
                break;
            }

            /* Full block */
            $c .= $block;
            $bytes -= 64;
            if ($bytes <= 0) {
                break;
            }
            $message = self::substr($message, 64);
        }
        /* end for(;;) loop */

        $ctx[12] = $j12;
        $ctx[13] = $j13;
        return $c;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $len
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function stream($len = 64, $nonce = '', $key = '')
    {
        return self::encryptBytes(
            new ParagonIE_Sodium_Core_ChaCha20_Ctx($key, $nonce),
            str_repeat("\x00", $len)
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $len
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ietfStream($len, $nonce = '', $key = '')
    {
        return self::encryptBytes(
            new ParagonIE_Sodium_Core_ChaCha20_IetfCtx($key, $nonce),
            str_repeat("\x00", $len)
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $nonce
     * @param string $key
     * @param string $ic
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ietfStreamXorIc($message, $nonce = '', $key = '', $ic = '')
    {
        return self::encryptBytes(
            new ParagonIE_Sodium_Core_ChaCha20_IetfCtx($key, $nonce, $ic),
            $message
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $nonce
     * @param string $key
     * @param string $ic
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function streamXorIc($message, $nonce = '', $key = '', $ic = '')
    {
        return self::encryptBytes(
            new ParagonIE_Sodium_Core_ChaCha20_Ctx($key, $nonce, $ic),
            $message
        );
    }
}
Core/Base64/UrlSafe.php000064400000017063152331075510010543 0ustar00<?php

/**
 * Class ParagonIE_Sodium_Core_Base64UrlSafe
 *
 *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
 *  Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
 */
class ParagonIE_Sodium_Core_Base64_UrlSafe
{
    // COPY ParagonIE_Sodium_Core_Base64_Common STARTING HERE
    /**
     * Encode into Base64
     *
     * Base64 character set "[A-Z][a-z][0-9]+/"
     *
     * @param string $src
     * @return string
     * @throws TypeError
     */
    public static function encode($src)
    {
        return self::doEncode($src, true);
    }

    /**
     * Encode into Base64, no = padding
     *
     * Base64 character set "[A-Z][a-z][0-9]+/"
     *
     * @param string $src
     * @return string
     * @throws TypeError
     */
    public static function encodeUnpadded($src)
    {
        return self::doEncode($src, false);
    }

    /**
     * @param string $src
     * @param bool $pad   Include = padding?
     * @return string
     * @throws TypeError
     */
    protected static function doEncode($src, $pad = true)
    {
        $dest = '';
        $srcLen = ParagonIE_Sodium_Core_Util::strlen($src);
        // Main loop (no padding):
        for ($i = 0; $i + 3 <= $srcLen; $i += 3) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, 3));
            $b0 = $chunk[1];
            $b1 = $chunk[2];
            $b2 = $chunk[3];

            $dest .=
                self::encode6Bits(               $b0 >> 2       ) .
                self::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
                self::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) .
                self::encode6Bits(  $b2                     & 63);
        }
        // The last chunk, which may have padding:
        if ($i < $srcLen) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, $srcLen - $i));
            $b0 = $chunk[1];
            if ($i + 1 < $srcLen) {
                $b1 = $chunk[2];
                $dest .=
                    self::encode6Bits($b0 >> 2) .
                    self::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
                    self::encode6Bits(($b1 << 2) & 63);
                if ($pad) {
                    $dest .= '=';
                }
            } else {
                $dest .=
                    self::encode6Bits( $b0 >> 2) .
                    self::encode6Bits(($b0 << 4) & 63);
                if ($pad) {
                    $dest .= '==';
                }
            }
        }
        return $dest;
    }

    /**
     * decode from base64 into binary
     *
     * Base64 character set "./[A-Z][a-z][0-9]"
     *
     * @param string $src
     * @param bool $strictPadding
     * @return string
     * @throws RangeException
     * @throws TypeError
     * @psalm-suppress RedundantCondition
     */
    public static function decode($src, $strictPadding = false)
    {
        // Remove padding
        $srcLen = ParagonIE_Sodium_Core_Util::strlen($src);
        if ($srcLen === 0) {
            return '';
        }

        if ($strictPadding) {
            if (($srcLen & 3) === 0) {
                if ($src[$srcLen - 1] === '=') {
                    $srcLen--;
                    if ($src[$srcLen - 1] === '=') {
                        $srcLen--;
                    }
                }
            }
            if (($srcLen & 3) === 1) {
                throw new RangeException(
                    'Incorrect padding'
                );
            }
            if ($src[$srcLen - 1] === '=') {
                throw new RangeException(
                    'Incorrect padding'
                );
            }
        } else {
            $src = rtrim($src, '=');
            $srcLen =  ParagonIE_Sodium_Core_Util::strlen($src);
        }

        $err = 0;
        $dest = '';
        // Main loop (no padding):
        for ($i = 0; $i + 4 <= $srcLen; $i += 4) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, 4));
            $c0 = self::decode6Bits($chunk[1]);
            $c1 = self::decode6Bits($chunk[2]);
            $c2 = self::decode6Bits($chunk[3]);
            $c3 = self::decode6Bits($chunk[4]);

            $dest .= pack(
                'CCC',
                ((($c0 << 2) | ($c1 >> 4)) & 0xff),
                ((($c1 << 4) | ($c2 >> 2)) & 0xff),
                ((($c2 << 6) | $c3) & 0xff)
            );
            $err |= ($c0 | $c1 | $c2 | $c3) >> 8;
        }
        // The last chunk, which may have padding:
        if ($i < $srcLen) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, $srcLen - $i));
            $c0 = self::decode6Bits($chunk[1]);

            if ($i + 2 < $srcLen) {
                $c1 = self::decode6Bits($chunk[2]);
                $c2 = self::decode6Bits($chunk[3]);
                $dest .= pack(
                    'CC',
                    ((($c0 << 2) | ($c1 >> 4)) & 0xff),
                    ((($c1 << 4) | ($c2 >> 2)) & 0xff)
                );
                $err |= ($c0 | $c1 | $c2) >> 8;
            } elseif ($i + 1 < $srcLen) {
                $c1 = self::decode6Bits($chunk[2]);
                $dest .= pack(
                    'C',
                    ((($c0 << 2) | ($c1 >> 4)) & 0xff)
                );
                $err |= ($c0 | $c1) >> 8;
            } elseif ($i < $srcLen && $strictPadding) {
                $err |= 1;
            }
        }
        /** @var bool $check */
        $check = ($err === 0);
        if (!$check) {
            throw new RangeException(
                'Base64::decode() only expects characters in the correct base64 alphabet'
            );
        }
        return $dest;
    }
    // COPY ParagonIE_Sodium_Core_Base64_Common ENDING HERE
    /**
     * Uses bitwise operators instead of table-lookups to turn 6-bit integers
     * into 8-bit integers.
     *
     * Base64 character set:
     * [A-Z]      [a-z]      [0-9]      +     /
     * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f
     *
     * @param int $src
     * @return int
     */
    protected static function decode6Bits($src)
    {
        $ret = -1;

        // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64
        $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);

        // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70
        $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);

        // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5
        $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);

        // if ($src == 0x2c) $ret += 62 + 1;
        $ret += (((0x2c - $src) & ($src - 0x2e)) >> 8) & 63;

        // if ($src == 0x5f) ret += 63 + 1;
        $ret += (((0x5e - $src) & ($src - 0x60)) >> 8) & 64;

        return $ret;
    }

    /**
     * Uses bitwise operators instead of table-lookups to turn 8-bit integers
     * into 6-bit integers.
     *
     * @param int $src
     * @return string
     */
    protected static function encode6Bits($src)
    {
        $diff = 0x41;

        // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6
        $diff += ((25 - $src) >> 8) & 6;

        // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75
        $diff -= ((51 - $src) >> 8) & 75;

        // if ($src > 61) $diff += 0x2d - 0x30 - 10; // -13
        $diff -= ((61 - $src) >> 8) & 13;

        // if ($src > 62) $diff += 0x5f - 0x2b - 1; // 3
        $diff += ((62 - $src) >> 8) & 49;

        return pack('C', $src + $diff);
    }
}
Core/Base64/Common.php000064400000015027152331075510010430 0ustar00<?php

/**
 * Class ParagonIE_Sodium_Core_Base64
 *
 *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
 *  Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
 *
 * We have to copy/paste the contents into the variant files because PHP 5.2
 * doesn't support late static binding, and we have no better workaround
 * available that won't break PHP 7+. Therefore, we're forced to duplicate code.
 */
abstract class ParagonIE_Sodium_Core_Base64_Common
{
    /**
     * Encode into Base64
     *
     * Base64 character set "[A-Z][a-z][0-9]+/"
     *
     * @param string $src
     * @return string
     * @throws TypeError
     */
    public static function encode($src)
    {
        return self::doEncode($src, true);
    }

    /**
     * Encode into Base64, no = padding
     *
     * Base64 character set "[A-Z][a-z][0-9]+/"
     *
     * @param string $src
     * @return string
     * @throws TypeError
     */
    public static function encodeUnpadded($src)
    {
        return self::doEncode($src, false);
    }

    /**
     * @param string $src
     * @param bool $pad   Include = padding?
     * @return string
     * @throws TypeError
     */
    protected static function doEncode($src, $pad = true)
    {
        $dest = '';
        $srcLen = ParagonIE_Sodium_Core_Util::strlen($src);
        // Main loop (no padding):
        for ($i = 0; $i + 3 <= $srcLen; $i += 3) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, 3));
            $b0 = $chunk[1];
            $b1 = $chunk[2];
            $b2 = $chunk[3];

            $dest .=
                self::encode6Bits(               $b0 >> 2       ) .
                self::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
                self::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) .
                self::encode6Bits(  $b2                     & 63);
        }
        // The last chunk, which may have padding:
        if ($i < $srcLen) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, $srcLen - $i));
            $b0 = $chunk[1];
            if ($i + 1 < $srcLen) {
                $b1 = $chunk[2];
                $dest .=
                    self::encode6Bits($b0 >> 2) .
                    self::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
                    self::encode6Bits(($b1 << 2) & 63);
                if ($pad) {
                    $dest .= '=';
                }
            } else {
                $dest .=
                    self::encode6Bits( $b0 >> 2) .
                    self::encode6Bits(($b0 << 4) & 63);
                if ($pad) {
                    $dest .= '==';
                }
            }
        }
        return $dest;
    }

    /**
     * decode from base64 into binary
     *
     * Base64 character set "./[A-Z][a-z][0-9]"
     *
     * @param string $src
     * @param bool $strictPadding
     * @return string
     * @throws RangeException
     * @throws TypeError
     * @psalm-suppress RedundantCondition
     */
    public static function decode($src, $strictPadding = false)
    {
        // Remove padding
        $srcLen = ParagonIE_Sodium_Core_Util::strlen($src);
        if ($srcLen === 0) {
            return '';
        }

        if ($strictPadding) {
            if (($srcLen & 3) === 0) {
                if ($src[$srcLen - 1] === '=') {
                    $srcLen--;
                    if ($src[$srcLen - 1] === '=') {
                        $srcLen--;
                    }
                }
            }
            if (($srcLen & 3) === 1) {
                throw new RangeException(
                    'Incorrect padding'
                );
            }
            if ($src[$srcLen - 1] === '=') {
                throw new RangeException(
                    'Incorrect padding'
                );
            }
        } else {
            $src = rtrim($src, '=');
            $srcLen = ParagonIE_Sodium_Core_Util::strlen($src);
        }

        $err = 0;
        $dest = '';
        // Main loop (no padding):
        for ($i = 0; $i + 4 <= $srcLen; $i += 4) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, 4));
            $c0 = self::decode6Bits($chunk[1]);
            $c1 = self::decode6Bits($chunk[2]);
            $c2 = self::decode6Bits($chunk[3]);
            $c3 = self::decode6Bits($chunk[4]);

            $dest .= pack(
                'CCC',
                ((($c0 << 2) | ($c1 >> 4)) & 0xff),
                ((($c1 << 4) | ($c2 >> 2)) & 0xff),
                ((($c2 << 6) |  $c3      ) & 0xff)
            );
            $err |= ($c0 | $c1 | $c2 | $c3) >> 8;
        }
        // The last chunk, which may have padding:
        if ($i < $srcLen) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, $srcLen - $i));
            $c0 = self::decode6Bits($chunk[1]);

            if ($i + 2 < $srcLen) {
                $c1 = self::decode6Bits($chunk[2]);
                $c2 = self::decode6Bits($chunk[3]);
                $dest .= pack(
                    'CC',
                    ((($c0 << 2) | ($c1 >> 4)) & 0xff),
                    ((($c1 << 4) | ($c2 >> 2)) & 0xff)
                );
                $err |= ($c0 | $c1 | $c2) >> 8;
            } elseif ($i + 1 < $srcLen) {
                $c1 = self::decode6Bits($chunk[2]);
                $dest .= pack(
                    'C',
                    ((($c0 << 2) | ($c1 >> 4)) & 0xff)
                );
                $err |= ($c0 | $c1) >> 8;
            } elseif ($i < $srcLen && $strictPadding) {
                $err |= 1;
            }
        }
        /** @var bool $check */
        $check = ($err === 0);
        if (!$check) {
            throw new RangeException(
                'Base64::decode() only expects characters in the correct base64 alphabet'
            );
        }
        return $dest;
    }

    /**
     * Uses bitwise operators instead of table-lookups to turn 6-bit integers
     * into 8-bit integers.
     *
     * Base64 character set:
     * [A-Z]      [a-z]      [0-9]      +     /
     * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f
     *
     * @param int $src
     * @return int
     */
    abstract protected static function decode6Bits($src);

    /**
     * Uses bitwise operators instead of table-lookups to turn 8-bit integers
     * into 6-bit integers.
     *
     * @param int $src
     * @return string
     */
    abstract protected static function encode6Bits($src);
}
Core/Base64/Original.php000064400000017055152331075510010747 0ustar00<?php

/**
 * Class ParagonIE_Sodium_Core_Base64
 *
 *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
 *  Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
 */
class ParagonIE_Sodium_Core_Base64_Original
{
    // COPY ParagonIE_Sodium_Core_Base64_Common STARTING HERE
    /**
     * Encode into Base64
     *
     * Base64 character set "[A-Z][a-z][0-9]+/"
     *
     * @param string $src
     * @return string
     * @throws TypeError
     */
    public static function encode($src)
    {
        return self::doEncode($src, true);
    }

    /**
     * Encode into Base64, no = padding
     *
     * Base64 character set "[A-Z][a-z][0-9]+/"
     *
     * @param string $src
     * @return string
     * @throws TypeError
     */
    public static function encodeUnpadded($src)
    {
        return self::doEncode($src, false);
    }

    /**
     * @param string $src
     * @param bool $pad   Include = padding?
     * @return string
     * @throws TypeError
     */
    protected static function doEncode($src, $pad = true)
    {
        $dest = '';
        $srcLen = ParagonIE_Sodium_Core_Util::strlen($src);
        // Main loop (no padding):
        for ($i = 0; $i + 3 <= $srcLen; $i += 3) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, 3));
            $b0 = $chunk[1];
            $b1 = $chunk[2];
            $b2 = $chunk[3];

            $dest .=
                self::encode6Bits(               $b0 >> 2       ) .
                self::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
                self::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) .
                self::encode6Bits(  $b2                     & 63);
        }
        // The last chunk, which may have padding:
        if ($i < $srcLen) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, $srcLen - $i));
            $b0 = $chunk[1];
            if ($i + 1 < $srcLen) {
                $b1 = $chunk[2];
                $dest .=
                    self::encode6Bits($b0 >> 2) .
                    self::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
                    self::encode6Bits(($b1 << 2) & 63);
                if ($pad) {
                    $dest .= '=';
                }
            } else {
                $dest .=
                    self::encode6Bits( $b0 >> 2) .
                    self::encode6Bits(($b0 << 4) & 63);
                if ($pad) {
                    $dest .= '==';
                }
            }
        }
        return $dest;
    }

    /**
     * decode from base64 into binary
     *
     * Base64 character set "./[A-Z][a-z][0-9]"
     *
     * @param string $src
     * @param bool $strictPadding
     * @return string
     * @throws RangeException
     * @throws TypeError
     * @psalm-suppress RedundantCondition
     */
    public static function decode($src, $strictPadding = false)
    {
        // Remove padding
        $srcLen = ParagonIE_Sodium_Core_Util::strlen($src);
        if ($srcLen === 0) {
            return '';
        }

        if ($strictPadding) {
            if (($srcLen & 3) === 0) {
                if ($src[$srcLen - 1] === '=') {
                    $srcLen--;
                    if ($src[$srcLen - 1] === '=') {
                        $srcLen--;
                    }
                }
            }
            if (($srcLen & 3) === 1) {
                throw new RangeException(
                    'Incorrect padding'
                );
            }
            if ($src[$srcLen - 1] === '=') {
                throw new RangeException(
                    'Incorrect padding'
                );
            }
        } else {
            $src = rtrim($src, '=');
            $srcLen =  ParagonIE_Sodium_Core_Util::strlen($src);
        }

        $err = 0;
        $dest = '';
        // Main loop (no padding):
        for ($i = 0; $i + 4 <= $srcLen; $i += 4) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, 4));
            $c0 = self::decode6Bits($chunk[1]);
            $c1 = self::decode6Bits($chunk[2]);
            $c2 = self::decode6Bits($chunk[3]);
            $c3 = self::decode6Bits($chunk[4]);

            $dest .= pack(
                'CCC',
                ((($c0 << 2) | ($c1 >> 4)) & 0xff),
                ((($c1 << 4) | ($c2 >> 2)) & 0xff),
                ((($c2 << 6) | $c3) & 0xff)
            );
            $err |= ($c0 | $c1 | $c2 | $c3) >> 8;
        }
        // The last chunk, which may have padding:
        if ($i < $srcLen) {
            /** @var array<int, int> $chunk */
            $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, $srcLen - $i));
            $c0 = self::decode6Bits($chunk[1]);

            if ($i + 2 < $srcLen) {
                $c1 = self::decode6Bits($chunk[2]);
                $c2 = self::decode6Bits($chunk[3]);
                $dest .= pack(
                    'CC',
                    ((($c0 << 2) | ($c1 >> 4)) & 0xff),
                    ((($c1 << 4) | ($c2 >> 2)) & 0xff)
                );
                $err |= ($c0 | $c1 | $c2) >> 8;
            } elseif ($i + 1 < $srcLen) {
                $c1 = self::decode6Bits($chunk[2]);
                $dest .= pack(
                    'C',
                    ((($c0 << 2) | ($c1 >> 4)) & 0xff)
                );
                $err |= ($c0 | $c1) >> 8;
            } elseif ($i < $srcLen && $strictPadding) {
                $err |= 1;
            }
        }
        /** @var bool $check */
        $check = ($err === 0);
        if (!$check) {
            throw new RangeException(
                'Base64::decode() only expects characters in the correct base64 alphabet'
            );
        }
        return $dest;
    }
    // COPY ParagonIE_Sodium_Core_Base64_Common ENDING HERE

    /**
     * Uses bitwise operators instead of table-lookups to turn 6-bit integers
     * into 8-bit integers.
     *
     * Base64 character set:
     * [A-Z]      [a-z]      [0-9]      +     /
     * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f
     *
     * @param int $src
     * @return int
     */
    protected static function decode6Bits($src)
    {
        $ret = -1;

        // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64
        $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);

        // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70
        $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);

        // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5
        $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);

        // if ($src == 0x2b) $ret += 62 + 1;
        $ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63;

        // if ($src == 0x2f) ret += 63 + 1;
        $ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64;

        return $ret;
    }

    /**
     * Uses bitwise operators instead of table-lookups to turn 8-bit integers
     * into 6-bit integers.
     *
     * @param int $src
     * @return string
     */
    protected static function encode6Bits($src)
    {
        $diff = 0x41;

        // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6
        $diff += ((25 - $src) >> 8) & 6;

        // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75
        $diff -= ((51 - $src) >> 8) & 75;

        // if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15
        $diff -= ((61 - $src) >> 8) & 15;

        // if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3
        $diff += ((62 - $src) >> 8) & 3;

        return pack('C', $src + $diff);
    }
}
Core/HSalsa20.php000064400000007131152331075510007466 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_HSalsa20', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_HSalsa20
 */
abstract class ParagonIE_Sodium_Core_HSalsa20 extends ParagonIE_Sodium_Core_Salsa20
{
    /**
     * Calculate an hsalsa20 hash of a single block
     *
     * HSalsa20 doesn't have a counter and will never be used for more than
     * one block (used to derive a subkey for xsalsa20).
     *
     * @internal You should not use this directly from another application
     *
     * @param string $in
     * @param string $k
     * @param string|null $c
     * @return string
     * @throws TypeError
     */
    public static function hsalsa20($in, $k, $c = null)
    {
        if ($c === null) {
            $x0  = 0x61707865;
            $x5  = 0x3320646e;
            $x10 = 0x79622d32;
            $x15 = 0x6b206574;
        } else {
            $x0  = self::load_4(self::substr($c, 0, 4));
            $x5  = self::load_4(self::substr($c, 4, 4));
            $x10 = self::load_4(self::substr($c, 8, 4));
            $x15 = self::load_4(self::substr($c, 12, 4));
        }
        $x1  = self::load_4(self::substr($k, 0, 4));
        $x2  = self::load_4(self::substr($k, 4, 4));
        $x3  = self::load_4(self::substr($k, 8, 4));
        $x4  = self::load_4(self::substr($k, 12, 4));
        $x11 = self::load_4(self::substr($k, 16, 4));
        $x12 = self::load_4(self::substr($k, 20, 4));
        $x13 = self::load_4(self::substr($k, 24, 4));
        $x14 = self::load_4(self::substr($k, 28, 4));
        $x6  = self::load_4(self::substr($in, 0, 4));
        $x7  = self::load_4(self::substr($in, 4, 4));
        $x8  = self::load_4(self::substr($in, 8, 4));
        $x9  = self::load_4(self::substr($in, 12, 4));

        for ($i = self::ROUNDS; $i > 0; $i -= 2) {
            $x4 ^= self::rotate($x0 + $x12, 7);
            $x8 ^= self::rotate($x4 + $x0, 9);
            $x12 ^= self::rotate($x8 + $x4, 13);
            $x0 ^= self::rotate($x12 + $x8, 18);
            $x9 ^= self::rotate($x5 + $x1, 7);
            $x13 ^= self::rotate($x9 + $x5, 9);
            $x1 ^= self::rotate($x13 + $x9, 13);
            $x5 ^= self::rotate($x1 + $x13, 18);
            $x14 ^= self::rotate($x10 + $x6, 7);
            $x2 ^= self::rotate($x14 + $x10, 9);
            $x6 ^= self::rotate($x2 + $x14, 13);
            $x10 ^= self::rotate($x6 + $x2, 18);
            $x3 ^= self::rotate($x15 + $x11, 7);
            $x7 ^= self::rotate($x3 + $x15, 9);
            $x11 ^= self::rotate($x7 + $x3, 13);
            $x15 ^= self::rotate($x11 + $x7, 18);
            $x1 ^= self::rotate($x0 + $x3, 7);
            $x2 ^= self::rotate($x1 + $x0, 9);
            $x3 ^= self::rotate($x2 + $x1, 13);
            $x0 ^= self::rotate($x3 + $x2, 18);
            $x6 ^= self::rotate($x5 + $x4, 7);
            $x7 ^= self::rotate($x6 + $x5, 9);
            $x4 ^= self::rotate($x7 + $x6, 13);
            $x5 ^= self::rotate($x4 + $x7, 18);
            $x11 ^= self::rotate($x10 + $x9, 7);
            $x8 ^= self::rotate($x11 + $x10, 9);
            $x9 ^= self::rotate($x8 + $x11, 13);
            $x10 ^= self::rotate($x9 + $x8, 18);
            $x12 ^= self::rotate($x15 + $x14, 7);
            $x13 ^= self::rotate($x12 + $x15, 9);
            $x14 ^= self::rotate($x13 + $x12, 13);
            $x15 ^= self::rotate($x14 + $x13, 18);
        }

        return self::store32_le($x0) .
            self::store32_le($x5) .
            self::store32_le($x10) .
            self::store32_le($x15) .
            self::store32_le($x6) .
            self::store32_le($x7) .
            self::store32_le($x8) .
            self::store32_le($x9);
    }
}
Core/Ristretto255.php000064400000052574152331075510010417 0ustar00<?php

/**
 * Class ParagonIE_Sodium_Core_Ristretto255
 */
class ParagonIE_Sodium_Core_Ristretto255 extends ParagonIE_Sodium_Core_Ed25519
{
    const crypto_core_ristretto255_HASHBYTES = 64;
    const HASH_SC_L = 48;
    const CORE_H2C_SHA256 = 1;
    const CORE_H2C_SHA512 = 2;

    /**
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @param int $b
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     */
    public static function fe_cneg(ParagonIE_Sodium_Core_Curve25519_Fe $f, $b)
    {
        $negf = self::fe_neg($f);
        return self::fe_cmov($f, $negf, $b);
    }

    /**
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @return ParagonIE_Sodium_Core_Curve25519_Fe
     * @throws SodiumException
     */
    public static function fe_abs(ParagonIE_Sodium_Core_Curve25519_Fe $f)
    {
        return self::fe_cneg($f, self::fe_isnegative($f));
    }

    /**
     * Returns 0 if this field element results in all NUL bytes.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $f
     * @return int
     * @throws SodiumException
     */
    public static function fe_iszero(ParagonIE_Sodium_Core_Curve25519_Fe $f)
    {
        static $zero;
        if ($zero === null) {
            $zero = str_repeat("\x00", 32);
        }
        /** @var string $zero */
        $str = self::fe_tobytes($f);

        $d = 0;
        for ($i = 0; $i < 32; ++$i) {
            $d |= self::chrToInt($str[$i]);
        }
        return (($d - 1) >> 31) & 1;
    }


    /**
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $u
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $v
     * @return array{x: ParagonIE_Sodium_Core_Curve25519_Fe, nonsquare: int}
     *
     * @throws SodiumException
     */
    public static function ristretto255_sqrt_ratio_m1(
        ParagonIE_Sodium_Core_Curve25519_Fe $u,
        ParagonIE_Sodium_Core_Curve25519_Fe $v
    ) {
        $sqrtm1 = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$sqrtm1);

        $v3 = self::fe_mul(
            self::fe_sq($v),
            $v
        ); /* v3 = v^3 */
        $x = self::fe_mul(
            self::fe_mul(
                self::fe_sq($v3),
                $u
            ),
            $v
        ); /* x = uv^7 */

        $x = self::fe_mul(
            self::fe_mul(
                self::fe_pow22523($x), /* x = (uv^7)^((q-5)/8) */
                $v3
            ),
            $u
        ); /* x = uv^3(uv^7)^((q-5)/8) */

        $vxx = self::fe_mul(
            self::fe_sq($x),
            $v
        ); /* vx^2 */

        $m_root_check = self::fe_sub($vxx, $u); /* vx^2-u */
        $p_root_check = self::fe_add($vxx, $u); /* vx^2+u */
        $f_root_check = self::fe_mul($u, $sqrtm1); /* u*sqrt(-1) */
        $f_root_check = self::fe_add($vxx, $f_root_check); /* vx^2+u*sqrt(-1) */

        $has_m_root = self::fe_iszero($m_root_check);
        $has_p_root = self::fe_iszero($p_root_check);
        $has_f_root = self::fe_iszero($f_root_check);

        $x_sqrtm1 = self::fe_mul($x, $sqrtm1); /* x*sqrt(-1) */

        $x = self::fe_abs(
            self::fe_cmov($x, $x_sqrtm1, $has_p_root | $has_f_root)
        );
        return array(
            'x' => $x,
            'nonsquare' => $has_m_root | $has_p_root
        );
    }

    /**
     * @param string $s
     * @return int
     * @throws SodiumException
     */
    public static function ristretto255_point_is_canonical($s)
    {
        $c = (self::chrToInt($s[31]) & 0x7f) ^ 0x7f;
        for ($i = 30; $i > 0; --$i) {
            $c |= self::chrToInt($s[$i]) ^ 0xff;
        }
        $c = ($c - 1) >> 8;
        $d = (0xed - 1 - self::chrToInt($s[0])) >> 8;
        $e = self::chrToInt($s[31]) >> 7;

        return 1 - ((($c & $d) | $e | self::chrToInt($s[0])) & 1);
    }

    /**
     * @param string $s
     * @param bool $skipCanonicalCheck
     * @return array{h: ParagonIE_Sodium_Core_Curve25519_Ge_P3, res: int}
     * @throws SodiumException
     */
    public static function ristretto255_frombytes($s, $skipCanonicalCheck = false)
    {
        if (!$skipCanonicalCheck) {
            if (!self::ristretto255_point_is_canonical($s)) {
                throw new SodiumException('S is not canonical');
            }
        }

        $s_ = self::fe_frombytes($s);
        $ss = self::fe_sq($s_); /* ss = s^2 */

        $u1 = self::fe_sub(self::fe_1(), $ss); /* u1 = 1-ss */
        $u1u1 = self::fe_sq($u1); /* u1u1 = u1^2 */

        $u2 = self::fe_add(self::fe_1(), $ss); /* u2 = 1+ss */
        $u2u2 = self::fe_sq($u2); /* u2u2 = u2^2 */

        $v = self::fe_mul(
            ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$d),
            $u1u1
        ); /* v = d*u1^2 */
        $v = self::fe_neg($v); /* v = -d*u1^2 */
        $v = self::fe_sub($v, $u2u2); /* v = -(d*u1^2)-u2^2 */
        $v_u2u2 = self::fe_mul($v, $u2u2); /* v_u2u2 = v*u2^2 */

        // fe25519_1(one);
        // notsquare = ristretto255_sqrt_ratio_m1(inv_sqrt, one, v_u2u2);
        $one = self::fe_1();
        $result = self::ristretto255_sqrt_ratio_m1($one, $v_u2u2);
        $inv_sqrt = $result['x'];
        $notsquare = $result['nonsquare'];

        $h = new ParagonIE_Sodium_Core_Curve25519_Ge_P3();

        $h->X = self::fe_mul($inv_sqrt, $u2);
        $h->Y = self::fe_mul(self::fe_mul($inv_sqrt, $h->X), $v);

        $h->X = self::fe_mul($h->X, $s_);
        $h->X = self::fe_abs(
            self::fe_add($h->X, $h->X)
        );
        $h->Y = self::fe_mul($u1, $h->Y);
        $h->Z = self::fe_1();
        $h->T = self::fe_mul($h->X, $h->Y);

        $res = - ((1 - $notsquare) | self::fe_isnegative($h->T) | self::fe_iszero($h->Y));
        return array('h' => $h, 'res' => $res);
    }

    /**
     * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $h
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_p3_tobytes(ParagonIE_Sodium_Core_Curve25519_Ge_P3 $h)
    {
        $sqrtm1 = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$sqrtm1);
        $invsqrtamd = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$invsqrtamd);

        $u1 = self::fe_add($h->Z, $h->Y); /* u1 = Z+Y */
        $zmy = self::fe_sub($h->Z, $h->Y); /* zmy = Z-Y */
        $u1 = self::fe_mul($u1, $zmy); /* u1 = (Z+Y)*(Z-Y) */
        $u2 = self::fe_mul($h->X, $h->Y); /* u2 = X*Y */

        $u1_u2u2 = self::fe_mul(self::fe_sq($u2), $u1); /* u1_u2u2 = u1*u2^2 */
        $one = self::fe_1();

        // fe25519_1(one);
        // (void) ristretto255_sqrt_ratio_m1(inv_sqrt, one, u1_u2u2);
        $result = self::ristretto255_sqrt_ratio_m1($one, $u1_u2u2);
        $inv_sqrt = $result['x'];

        $den1 = self::fe_mul($inv_sqrt, $u1); /* den1 = inv_sqrt*u1 */
        $den2 = self::fe_mul($inv_sqrt, $u2); /* den2 = inv_sqrt*u2 */
        $z_inv = self::fe_mul($h->T, self::fe_mul($den1, $den2)); /* z_inv = den1*den2*T */

        $ix = self::fe_mul($h->X, $sqrtm1); /* ix = X*sqrt(-1) */
        $iy = self::fe_mul($h->Y, $sqrtm1); /* iy = Y*sqrt(-1) */
        $eden = self::fe_mul($den1, $invsqrtamd);

        $t_z_inv =  self::fe_mul($h->T, $z_inv); /* t_z_inv = T*z_inv */
        $rotate = self::fe_isnegative($t_z_inv);

        $x_ = self::fe_copy($h->X);
        $y_ = self::fe_copy($h->Y);
        $den_inv = self::fe_copy($den2);

        $x_ = self::fe_cmov($x_, $iy, $rotate);
        $y_ = self::fe_cmov($y_, $ix, $rotate);
        $den_inv = self::fe_cmov($den_inv, $eden, $rotate);

        $x_z_inv = self::fe_mul($x_, $z_inv);
        $y_ = self::fe_cneg($y_, self::fe_isnegative($x_z_inv));


        // fe25519_sub(s_, h->Z, y_);
        // fe25519_mul(s_, den_inv, s_);
        // fe25519_abs(s_, s_);
        // fe25519_tobytes(s, s_);
        return self::fe_tobytes(
            self::fe_abs(
                self::fe_mul(
                    $den_inv,
                    self::fe_sub($h->Z, $y_)
                )
            )
        );
    }

    /**
     * @param ParagonIE_Sodium_Core_Curve25519_Fe $t
     * @return ParagonIE_Sodium_Core_Curve25519_Ge_P3
     *
     * @throws SodiumException
     */
    public static function ristretto255_elligator(ParagonIE_Sodium_Core_Curve25519_Fe $t)
    {
        $sqrtm1   = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$sqrtm1);
        $onemsqd  = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$onemsqd);
        $d        = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$d);
        $sqdmone  = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$sqdmone);
        $sqrtadm1 = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$sqrtadm1);

        $one = self::fe_1();
        $r   = self::fe_mul($sqrtm1, self::fe_sq($t));         /* r = sqrt(-1)*t^2 */
        $u   = self::fe_mul(self::fe_add($r, $one), $onemsqd); /* u = (r+1)*(1-d^2) */
        $c   = self::fe_neg(self::fe_1());                     /* c = -1 */
        $rpd = self::fe_add($r, $d);                           /* rpd = r+d */

        $v = self::fe_mul(
            self::fe_sub(
                $c,
                self::fe_mul($r, $d)
            ),
            $rpd
        ); /* v = (c-r*d)*(r+d) */

        $result = self::ristretto255_sqrt_ratio_m1($u, $v);
        $s = $result['x'];
        $wasnt_square = 1 - $result['nonsquare'];

        $s_prime = self::fe_neg(
            self::fe_abs(
                self::fe_mul($s, $t)
            )
        ); /* s_prime = -|s*t| */
        $s = self::fe_cmov($s, $s_prime, $wasnt_square);
        $c = self::fe_cmov($c, $r, $wasnt_square);

        // fe25519_sub(n, r, one);            /* n = r-1 */
        // fe25519_mul(n, n, c);              /* n = c*(r-1) */
        // fe25519_mul(n, n, ed25519_sqdmone); /* n = c*(r-1)*(d-1)^2 */
        // fe25519_sub(n, n, v);              /* n =  c*(r-1)*(d-1)^2-v */
        $n = self::fe_sub(
            self::fe_mul(
                self::fe_mul(
                    self::fe_sub($r, $one),
                    $c
                ),
                $sqdmone
            ),
            $v
        ); /* n =  c*(r-1)*(d-1)^2-v */

        $w0 = self::fe_mul(
            self::fe_add($s, $s),
            $v
        ); /* w0 = 2s*v */

        $w1 = self::fe_mul($n, $sqrtadm1); /* w1 = n*sqrt(ad-1) */
        $ss = self::fe_sq($s); /* ss = s^2 */
        $w2 = self::fe_sub($one, $ss); /* w2 = 1-s^2 */
        $w3 = self::fe_add($one, $ss); /* w3 = 1+s^2 */

        return new ParagonIE_Sodium_Core_Curve25519_Ge_P3(
            self::fe_mul($w0, $w3),
            self::fe_mul($w2, $w1),
            self::fe_mul($w1, $w3),
            self::fe_mul($w0, $w2)
        );
    }

    /**
     * @param string $h
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_from_hash($h)
    {
        if (self::strlen($h) !== 64) {
            throw new SodiumException('Hash must be 64 bytes');
        }
        //fe25519_frombytes(r0, h);
        //fe25519_frombytes(r1, h + 32);
        $r0 = self::fe_frombytes(self::substr($h, 0, 32));
        $r1 = self::fe_frombytes(self::substr($h, 32, 32));

        //ristretto255_elligator(&p0, r0);
        //ristretto255_elligator(&p1, r1);
        $p0 = self::ristretto255_elligator($r0);
        $p1 = self::ristretto255_elligator($r1);

        //ge25519_p3_to_cached(&p1_cached, &p1);
        //ge25519_add_cached(&p_p1p1, &p0, &p1_cached);
        $p_p1p1 = self::ge_add(
            $p0,
            self::ge_p3_to_cached($p1)
        );

        //ge25519_p1p1_to_p3(&p, &p_p1p1);
        //ristretto255_p3_tobytes(s, &p);
        return self::ristretto255_p3_tobytes(
            self::ge_p1p1_to_p3($p_p1p1)
        );
    }

    /**
     * @param string $p
     * @return int
     * @throws SodiumException
     */
    public static function is_valid_point($p)
    {
        $result = self::ristretto255_frombytes($p);
        if ($result['res'] !== 0) {
            return 0;
        }
        return 1;
    }

    /**
     * @param string $p
     * @param string $q
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_add($p, $q)
    {
        $p_res = self::ristretto255_frombytes($p);
        $q_res = self::ristretto255_frombytes($q);
        if ($p_res['res'] !== 0 || $q_res['res'] !== 0) {
            throw new SodiumException('Could not add points');
        }
        $p_p3 = $p_res['h'];
        $q_p3 = $q_res['h'];
        $q_cached = self::ge_p3_to_cached($q_p3);
        $r_p1p1 = self::ge_add($p_p3, $q_cached);
        $r_p3 = self::ge_p1p1_to_p3($r_p1p1);
        return self::ristretto255_p3_tobytes($r_p3);
    }

    /**
     * @param string $p
     * @param string $q
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_sub($p, $q)
    {
        $p_res = self::ristretto255_frombytes($p);
        $q_res = self::ristretto255_frombytes($q);
        if ($p_res['res'] !== 0 || $q_res['res'] !== 0) {
            throw new SodiumException('Could not add points');
        }
        $p_p3 = $p_res['h'];
        $q_p3 = $q_res['h'];
        $q_cached = self::ge_p3_to_cached($q_p3);
        $r_p1p1 = self::ge_sub($p_p3, $q_cached);
        $r_p3 = self::ge_p1p1_to_p3($r_p1p1);
        return self::ristretto255_p3_tobytes($r_p3);
    }


    /**
     * @param int $hLen
     * @param ?string $ctx
     * @param string $msg
     * @return string
     * @throws SodiumException
     * @psalm-suppress PossiblyInvalidArgument hash API
     */
    protected static function h2c_string_to_hash_sha256($hLen, $ctx, $msg)
    {
        $h = array_fill(0, $hLen, 0);
        $ctx_len = !is_null($ctx) ? self::strlen($ctx) : 0;
        if ($hLen > 0xff) {
            throw new SodiumException('Hash must be less than 256 bytes');
        }

        if ($ctx_len > 0xff) {
            $st = hash_init('sha256');
            self::hash_update($st, "H2C-OVERSIZE-DST-");
            self::hash_update($st, $ctx);
            $ctx = hash_final($st, true);
            $ctx_len = 32;
        }
        $t = array(0, $hLen, 0);
        $ux = str_repeat("\0", 64);
        $st = hash_init('sha256');
        self::hash_update($st, $ux);
        self::hash_update($st, $msg);
        self::hash_update($st, self::intArrayToString($t));
        self::hash_update($st, $ctx);
        self::hash_update($st, self::intToChr($ctx_len));
        $u0 = hash_final($st, true);

        for ($i = 0; $i < $hLen; $i += 64) {
            $ux = self::xorStrings($ux, $u0);
            ++$t[2];
            $st = hash_init('sha256');
            self::hash_update($st, $ux);
            self::hash_update($st, self::intToChr($t[2]));
            self::hash_update($st, $ctx);
            self::hash_update($st, self::intToChr($ctx_len));
            $ux = hash_final($st, true);
            $amount = min($hLen - $i, 64);
            for ($j = 0; $j < $amount; ++$j) {
                $h[$i + $j] = self::chrToInt($ux[$i]);
            }
        }
        return self::intArrayToString(array_slice($h, 0, $hLen));
    }

    /**
     * @param int $hLen
     * @param ?string $ctx
     * @param string $msg
     * @return string
     * @throws SodiumException
     * @psalm-suppress PossiblyInvalidArgument hash API
     */
    protected static function h2c_string_to_hash_sha512($hLen, $ctx, $msg)
    {
        $h = array_fill(0, $hLen, 0);
        $ctx_len = !is_null($ctx) ? self::strlen($ctx) : 0;
        if ($hLen > 0xff) {
            throw new SodiumException('Hash must be less than 256 bytes');
        }

        if ($ctx_len > 0xff) {
            $st = hash_init('sha256');
            self::hash_update($st, "H2C-OVERSIZE-DST-");
            self::hash_update($st, $ctx);
            $ctx = hash_final($st, true);
            $ctx_len = 32;
        }
        $t = array(0, $hLen, 0);
        $ux = str_repeat("\0", 128);
        $st = hash_init('sha512');
        self::hash_update($st, $ux);
        self::hash_update($st, $msg);
        self::hash_update($st, self::intArrayToString($t));
        self::hash_update($st, $ctx);
        self::hash_update($st, self::intToChr($ctx_len));
        $u0 = hash_final($st, true);

        for ($i = 0; $i < $hLen; $i += 128) {
            $ux = self::xorStrings($ux, $u0);
            ++$t[2];
            $st = hash_init('sha512');
            self::hash_update($st, $ux);
            self::hash_update($st, self::intToChr($t[2]));
            self::hash_update($st, $ctx);
            self::hash_update($st, self::intToChr($ctx_len));
            $ux = hash_final($st, true);
            $amount = min($hLen - $i, 128);
            for ($j = 0; $j < $amount; ++$j) {
                $h[$i + $j] = self::chrToInt($ux[$i]);
            }
        }
        return self::intArrayToString(array_slice($h, 0, $hLen));
    }

    /**
     * @param int $hLen
     * @param ?string $ctx
     * @param string $msg
     * @param int $hash_alg
     * @return string
     * @throws SodiumException
     */
    public static function h2c_string_to_hash($hLen, $ctx, $msg, $hash_alg)
    {
        switch ($hash_alg) {
            case self::CORE_H2C_SHA256:
                return self::h2c_string_to_hash_sha256($hLen, $ctx, $msg);
            case self::CORE_H2C_SHA512:
                return self::h2c_string_to_hash_sha512($hLen, $ctx, $msg);
            default:
                throw new SodiumException('Invalid H2C hash algorithm');
        }
    }

    /**
     * @param ?string $ctx
     * @param string $msg
     * @param int $hash_alg
     * @return string
     * @throws SodiumException
     */
    protected static function _string_to_element($ctx, $msg, $hash_alg)
    {
        return self::ristretto255_from_hash(
            self::h2c_string_to_hash(self::crypto_core_ristretto255_HASHBYTES, $ctx, $msg, $hash_alg)
        );
    }

    /**
     * @return string
     * @throws SodiumException
     * @throws Exception
     */
    public static function ristretto255_random()
    {
        return self::ristretto255_from_hash(
            ParagonIE_Sodium_Compat::randombytes_buf(self::crypto_core_ristretto255_HASHBYTES)
        );
    }

    /**
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_scalar_random()
    {
        return self::scalar_random();
    }

    /**
     * @param string $s
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_scalar_complement($s)
    {
        return self::scalar_complement($s);
    }


    /**
     * @param string $s
     * @return string
     */
    public static function ristretto255_scalar_invert($s)
    {
        return self::sc25519_invert($s);
    }

    /**
     * @param string $s
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_scalar_negate($s)
    {
        return self::scalar_negate($s);
    }

    /**
     * @param string $x
     * @param string $y
     * @return string
     */
    public static function ristretto255_scalar_add($x, $y)
    {
        return self::scalar_add($x, $y);
    }

    /**
     * @param string $x
     * @param string $y
     * @return string
     */
    public static function ristretto255_scalar_sub($x, $y)
    {
        return self::scalar_sub($x, $y);
    }

    /**
     * @param string $x
     * @param string $y
     * @return string
     */
    public static function ristretto255_scalar_mul($x, $y)
    {
        return self::sc25519_mul($x, $y);
    }

    /**
     * @param string $ctx
     * @param string $msg
     * @param int $hash_alg
     * @return string
     * @throws SodiumException
     */
    public static function ristretto255_scalar_from_string($ctx, $msg, $hash_alg)
    {
        $h = array_fill(0, 64, 0);
        $h_be = self::stringToIntArray(
            self::h2c_string_to_hash(
                self::HASH_SC_L, $ctx, $msg, $hash_alg
            )
        );

        for ($i = 0; $i < self::HASH_SC_L; ++$i) {
            $h[$i] = $h_be[self::HASH_SC_L - 1 - $i];
        }
        return self::ristretto255_scalar_reduce(self::intArrayToString($h));
    }

    /**
     * @param string $s
     * @return string
     */
    public static function ristretto255_scalar_reduce($s)
    {
        return self::sc_reduce($s);
    }

    /**
     * @param string $n
     * @param string $p
     * @return string
     * @throws SodiumException
     */
    public static function scalarmult_ristretto255($n, $p)
    {
        if (self::strlen($n) !== 32) {
            throw new SodiumException('Scalar must be 32 bytes, ' . self::strlen($p) . ' given.');
        }
        if (self::strlen($p) !== 32) {
            throw new SodiumException('Point must be 32 bytes, ' . self::strlen($p) . ' given.');
        }
        $result = self::ristretto255_frombytes($p);
        if ($result['res'] !== 0) {
            throw new SodiumException('Could not multiply points');
        }
        $P = $result['h'];

        $t = self::stringToIntArray($n);
        $t[31] &= 0x7f;
        $Q = self::ge_scalarmult(self::intArrayToString($t), $P);
        $q = self::ristretto255_p3_tobytes($Q);
        if (ParagonIE_Sodium_Compat::is_zero($q)) {
            throw new SodiumException('An unknown error has occurred');
        }
        return $q;
    }

    /**
     * @param string $n
     * @return string
     * @throws SodiumException
     */
    public static function scalarmult_ristretto255_base($n)
    {
        $t = self::stringToIntArray($n);
        $t[31] &= 0x7f;
        $Q = self::ge_scalarmult_base(self::intArrayToString($t));
        $q = self::ristretto255_p3_tobytes($Q);
        if (ParagonIE_Sodium_Compat::is_zero($q)) {
            throw new SodiumException('An unknown error has occurred');
        }
        return $q;
    }
}
File.php000064400000150275152331075510006150 0ustar00<?php

if (class_exists('ParagonIE_Sodium_File', false)) {
    return;
}
/**
 * Class ParagonIE_Sodium_File
 */
class ParagonIE_Sodium_File extends ParagonIE_Sodium_Core_Util
{
    /* PHP's default buffer size is 8192 for fread()/fwrite(). */
    const BUFFER_SIZE = 8192;

    /**
     * Box a file (rather than a string). Uses less memory than
     * ParagonIE_Sodium_Compat::crypto_box(), but produces
     * the same result.
     *
     * @param string $inputFile  Absolute path to a file on the filesystem
     * @param string $outputFile Absolute path to a file on the filesystem
     * @param string $nonce      Number to be used only once
     * @param string $keyPair    ECDH secret key and ECDH public key concatenated
     *
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box($inputFile, $outputFile, $nonce, $keyPair)
    {
        /* Type checks: */
        if (!is_string($inputFile)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
        }
        if (!is_string($outputFile)) {
            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
        }
        if (!is_string($nonce)) {
            throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
        }

        /* Input validation: */
        if (!is_string($keyPair)) {
            throw new TypeError('Argument 4 must be a string, ' . gettype($keyPair) . ' given.');
        }
        if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_NONCEBYTES) {
            throw new TypeError('Argument 3 must be CRYPTO_BOX_NONCEBYTES bytes');
        }
        if (self::strlen($keyPair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) {
            throw new TypeError('Argument 4 must be CRYPTO_BOX_KEYPAIRBYTES bytes');
        }

        /** @var int $size */
        $size = filesize($inputFile);
        if (!is_int($size)) {
            throw new SodiumException('Could not obtain the file size');
        }

        /** @var resource $ifp */
        $ifp = fopen($inputFile, 'rb');
        if (!is_resource($ifp)) {
            throw new SodiumException('Could not open input file for reading');
        }

        /** @var resource $ofp */
        $ofp = fopen($outputFile, 'wb');
        if (!is_resource($ofp)) {
            fclose($ifp);
            throw new SodiumException('Could not open output file for writing');
        }

        $res = self::box_encrypt($ifp, $ofp, $size, $nonce, $keyPair);
        fclose($ifp);
        fclose($ofp);
        return $res;
    }

    /**
     * Open a boxed file (rather than a string). Uses less memory than
     * ParagonIE_Sodium_Compat::crypto_box_open(), but produces
     * the same result.
     *
     * Warning: Does not protect against TOCTOU attacks. You should
     * just load the file into memory and use crypto_box_open() if
     * you are worried about those.
     *
     * @param string $inputFile
     * @param string $outputFile
     * @param string $nonce
     * @param string $keypair
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_open($inputFile, $outputFile, $nonce, $keypair)
    {
        /* Type checks: */
        if (!is_string($inputFile)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
        }
        if (!is_string($outputFile)) {
            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
        }
        if (!is_string($nonce)) {
            throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
        }
        if (!is_string($keypair)) {
            throw new TypeError('Argument 4 must be a string, ' . gettype($keypair) . ' given.');
        }

        /* Input validation: */
        if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_NONCEBYTES) {
            throw new TypeError('Argument 4 must be CRYPTO_BOX_NONCEBYTES bytes');
        }
        if (self::strlen($keypair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) {
            throw new TypeError('Argument 4 must be CRYPTO_BOX_KEYPAIRBYTES bytes');
        }

        /** @var int $size */
        $size = filesize($inputFile);
        if (!is_int($size)) {
            throw new SodiumException('Could not obtain the file size');
        }

        /** @var resource $ifp */
        $ifp = fopen($inputFile, 'rb');
        if (!is_resource($ifp)) {
            throw new SodiumException('Could not open input file for reading');
        }

        /** @var resource $ofp */
        $ofp = fopen($outputFile, 'wb');
        if (!is_resource($ofp)) {
            fclose($ifp);
            throw new SodiumException('Could not open output file for writing');
        }

        $res = self::box_decrypt($ifp, $ofp, $size, $nonce, $keypair);
        fclose($ifp);
        fclose($ofp);
        try {
            ParagonIE_Sodium_Compat::memzero($nonce);
            ParagonIE_Sodium_Compat::memzero($ephKeypair);
        } catch (SodiumException $ex) {
            if (isset($ephKeypair)) {
                unset($ephKeypair);
            }
        }
        return $res;
    }

    /**
     * Seal a file (rather than a string). Uses less memory than
     * ParagonIE_Sodium_Compat::crypto_box_seal(), but produces
     * the same result.
     *
     * @param string $inputFile  Absolute path to a file on the filesystem
     * @param string $outputFile Absolute path to a file on the filesystem
     * @param string $publicKey  ECDH public key
     *
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_seal($inputFile, $outputFile, $publicKey)
    {
        /* Type checks: */
        if (!is_string($inputFile)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
        }
        if (!is_string($outputFile)) {
            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
        }
        if (!is_string($publicKey)) {
            throw new TypeError('Argument 3 must be a string, ' . gettype($publicKey) . ' given.');
        }

        /* Input validation: */
        if (self::strlen($publicKey) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) {
            throw new TypeError('Argument 3 must be CRYPTO_BOX_PUBLICKEYBYTES bytes');
        }

        /** @var int $size */
        $size = filesize($inputFile);
        if (!is_int($size)) {
            throw new SodiumException('Could not obtain the file size');
        }

        /** @var resource $ifp */
        $ifp = fopen($inputFile, 'rb');
        if (!is_resource($ifp)) {
            throw new SodiumException('Could not open input file for reading');
        }

        /** @var resource $ofp */
        $ofp = fopen($outputFile, 'wb');
        if (!is_resource($ofp)) {
            fclose($ifp);
            throw new SodiumException('Could not open output file for writing');
        }

        /** @var string $ephKeypair */
        $ephKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair();

        /** @var string $msgKeypair */
        $msgKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair_from_secretkey_and_publickey(
            ParagonIE_Sodium_Compat::crypto_box_secretkey($ephKeypair),
            $publicKey
        );

        /** @var string $ephemeralPK */
        $ephemeralPK = ParagonIE_Sodium_Compat::crypto_box_publickey($ephKeypair);

        /** @var string $nonce */
        $nonce = ParagonIE_Sodium_Compat::crypto_generichash(
            $ephemeralPK . $publicKey,
            '',
            24
        );

        /** @var int $firstWrite */
        $firstWrite = fwrite(
            $ofp,
            $ephemeralPK,
            ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES
        );
        if (!is_int($firstWrite)) {
            fclose($ifp);
            fclose($ofp);
            ParagonIE_Sodium_Compat::memzero($ephKeypair);
            throw new SodiumException('Could not write to output file');
        }
        if ($firstWrite !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) {
            ParagonIE_Sodium_Compat::memzero($ephKeypair);
            fclose($ifp);
            fclose($ofp);
            throw new SodiumException('Error writing public key to output file');
        }

        $res = self::box_encrypt($ifp, $ofp, $size, $nonce, $msgKeypair);
        fclose($ifp);
        fclose($ofp);
        try {
            ParagonIE_Sodium_Compat::memzero($nonce);
            ParagonIE_Sodium_Compat::memzero($ephKeypair);
        } catch (SodiumException $ex) {
            /** @psalm-suppress PossiblyUndefinedVariable */
            unset($ephKeypair);
        }
        return $res;
    }

    /**
     * Open a sealed file (rather than a string). Uses less memory than
     * ParagonIE_Sodium_Compat::crypto_box_seal_open(), but produces
     * the same result.
     *
     * Warning: Does not protect against TOCTOU attacks. You should
     * just load the file into memory and use crypto_box_seal_open() if
     * you are worried about those.
     *
     * @param string $inputFile
     * @param string $outputFile
     * @param string $ecdhKeypair
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_seal_open($inputFile, $outputFile, $ecdhKeypair)
    {
        /* Type checks: */
        if (!is_string($inputFile)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
        }
        if (!is_string($outputFile)) {
            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
        }
        if (!is_string($ecdhKeypair)) {
            throw new TypeError('Argument 3 must be a string, ' . gettype($ecdhKeypair) . ' given.');
        }

        /* Input validation: */
        if (self::strlen($ecdhKeypair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) {
            throw new TypeError('Argument 3 must be CRYPTO_BOX_KEYPAIRBYTES bytes');
        }

        $publicKey = ParagonIE_Sodium_Compat::crypto_box_publickey($ecdhKeypair);

        /** @var int $size */
        $size = filesize($inputFile);
        if (!is_int($size)) {
            throw new SodiumException('Could not obtain the file size');
        }

        /** @var resource $ifp */
        $ifp = fopen($inputFile, 'rb');
        if (!is_resource($ifp)) {
            throw new SodiumException('Could not open input file for reading');
        }

        /** @var resource $ofp */
        $ofp = fopen($outputFile, 'wb');
        if (!is_resource($ofp)) {
            fclose($ifp);
            throw new SodiumException('Could not open output file for writing');
        }

        $ephemeralPK = fread($ifp, ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES);
        if (!is_string($ephemeralPK)) {
            throw new SodiumException('Could not read input file');
        }
        if (self::strlen($ephemeralPK) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) {
            fclose($ifp);
            fclose($ofp);
            throw new SodiumException('Could not read public key from sealed file');
        }

        $nonce = ParagonIE_Sodium_Compat::crypto_generichash(
            $ephemeralPK . $publicKey,
            '',
            24
        );
        $msgKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair_from_secretkey_and_publickey(
            ParagonIE_Sodium_Compat::crypto_box_secretkey($ecdhKeypair),
            $ephemeralPK
        );

        $res = self::box_decrypt($ifp, $ofp, $size, $nonce, $msgKeypair);
        fclose($ifp);
        fclose($ofp);
        try {
            ParagonIE_Sodium_Compat::memzero($nonce);
            ParagonIE_Sodium_Compat::memzero($ephKeypair);
        } catch (SodiumException $ex) {
            if (isset($ephKeypair)) {
                unset($ephKeypair);
            }
        }
        return $res;
    }

    /**
     * Calculate the BLAKE2b hash of a file.
     *
     * @param string      $filePath     Absolute path to a file on the filesystem
     * @param string|null $key          BLAKE2b key
     * @param int         $outputLength Length of hash output
     *
     * @return string                   BLAKE2b hash
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress FailedTypeResolution
     */
    public static function generichash($filePath, $key = '', $outputLength = 32)
    {
        /* Type checks: */
        if (!is_string($filePath)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($filePath) . ' given.');
        }
        if (!is_string($key)) {
            if (is_null($key)) {
                $key = '';
            } else {
                throw new TypeError('Argument 2 must be a string, ' . gettype($key) . ' given.');
            }
        }
        if (!is_int($outputLength)) {
            if (!is_numeric($outputLength)) {
                throw new TypeError('Argument 3 must be an integer, ' . gettype($outputLength) . ' given.');
            }
            $outputLength = (int) $outputLength;
        }

        /* Input validation: */
        if (!empty($key)) {
            if (self::strlen($key) < ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_KEYBYTES_MIN) {
                throw new TypeError('Argument 2 must be at least CRYPTO_GENERICHASH_KEYBYTES_MIN bytes');
            }
            if (self::strlen($key) > ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_KEYBYTES_MAX) {
                throw new TypeError('Argument 2 must be at most CRYPTO_GENERICHASH_KEYBYTES_MAX bytes');
            }
        }
        if ($outputLength < ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_BYTES_MIN) {
            throw new SodiumException('Argument 3 must be at least CRYPTO_GENERICHASH_BYTES_MIN');
        }
        if ($outputLength > ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_BYTES_MAX) {
            throw new SodiumException('Argument 3 must be at least CRYPTO_GENERICHASH_BYTES_MAX');
        }

        /** @var int $size */
        $size = filesize($filePath);
        if (!is_int($size)) {
            throw new SodiumException('Could not obtain the file size');
        }

        /** @var resource $fp */
        $fp = fopen($filePath, 'rb');
        if (!is_resource($fp)) {
            throw new SodiumException('Could not open input file for reading');
        }
        $ctx = ParagonIE_Sodium_Compat::crypto_generichash_init($key, $outputLength);
        while ($size > 0) {
            $blockSize = $size > 64
                ? 64
                : $size;
            $read = fread($fp, $blockSize);
            if (!is_string($read)) {
                throw new SodiumException('Could not read input file');
            }
            ParagonIE_Sodium_Compat::crypto_generichash_update($ctx, $read);
            $size -= $blockSize;
        }

        fclose($fp);
        return ParagonIE_Sodium_Compat::crypto_generichash_final($ctx, $outputLength);
    }

    /**
     * Encrypt a file (rather than a string). Uses less memory than
     * ParagonIE_Sodium_Compat::crypto_secretbox(), but produces
     * the same result.
     *
     * @param string $inputFile  Absolute path to a file on the filesystem
     * @param string $outputFile Absolute path to a file on the filesystem
     * @param string $nonce      Number to be used only once
     * @param string $key        Encryption key
     *
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function secretbox($inputFile, $outputFile, $nonce, $key)
    {
        /* Type checks: */
        if (!is_string($inputFile)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given..');
        }
        if (!is_string($outputFile)) {
            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
        }
        if (!is_string($nonce)) {
            throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
        }

        /* Input validation: */
        if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_NONCEBYTES) {
            throw new TypeError('Argument 3 must be CRYPTO_SECRETBOX_NONCEBYTES bytes');
        }
        if (!is_string($key)) {
            throw new TypeError('Argument 4 must be a string, ' . gettype($key) . ' given.');
        }
        if (self::strlen($key) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_KEYBYTES) {
            throw new TypeError('Argument 4 must be CRYPTO_SECRETBOX_KEYBYTES bytes');
        }

        /** @var int $size */
        $size = filesize($inputFile);
        if (!is_int($size)) {
            throw new SodiumException('Could not obtain the file size');
        }

        /** @var resource $ifp */
        $ifp = fopen($inputFile, 'rb');
        if (!is_resource($ifp)) {
            throw new SodiumException('Could not open input file for reading');
        }

        /** @var resource $ofp */
        $ofp = fopen($outputFile, 'wb');
        if (!is_resource($ofp)) {
            fclose($ifp);
            throw new SodiumException('Could not open output file for writing');
        }

        $res = self::secretbox_encrypt($ifp, $ofp, $size, $nonce, $key);
        fclose($ifp);
        fclose($ofp);
        return $res;
    }
    /**
     * Seal a file (rather than a string). Uses less memory than
     * ParagonIE_Sodium_Compat::crypto_secretbox_open(), but produces
     * the same result.
     *
     * Warning: Does not protect against TOCTOU attacks. You should
     * just load the file into memory and use crypto_secretbox_open() if
     * you are worried about those.
     *
     * @param string $inputFile
     * @param string $outputFile
     * @param string $nonce
     * @param string $key
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function secretbox_open($inputFile, $outputFile, $nonce, $key)
    {
        /* Type checks: */
        if (!is_string($inputFile)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
        }
        if (!is_string($outputFile)) {
            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
        }
        if (!is_string($nonce)) {
            throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
        }
        if (!is_string($key)) {
            throw new TypeError('Argument 4 must be a string, ' . gettype($key) . ' given.');
        }

        /* Input validation: */
        if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_NONCEBYTES) {
            throw new TypeError('Argument 4 must be CRYPTO_SECRETBOX_NONCEBYTES bytes');
        }
        if (self::strlen($key) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_KEYBYTES) {
            throw new TypeError('Argument 4 must be CRYPTO_SECRETBOXBOX_KEYBYTES bytes');
        }

        /** @var int $size */
        $size = filesize($inputFile);
        if (!is_int($size)) {
            throw new SodiumException('Could not obtain the file size');
        }

        /** @var resource $ifp */
        $ifp = fopen($inputFile, 'rb');
        if (!is_resource($ifp)) {
            throw new SodiumException('Could not open input file for reading');
        }

        /** @var resource $ofp */
        $ofp = fopen($outputFile, 'wb');
        if (!is_resource($ofp)) {
            fclose($ifp);
            throw new SodiumException('Could not open output file for writing');
        }

        $res = self::secretbox_decrypt($ifp, $ofp, $size, $nonce, $key);
        fclose($ifp);
        fclose($ofp);
        try {
            ParagonIE_Sodium_Compat::memzero($key);
        } catch (SodiumException $ex) {
            /** @psalm-suppress PossiblyUndefinedVariable */
            unset($key);
        }
        return $res;
    }

    /**
     * Sign a file (rather than a string). Uses less memory than
     * ParagonIE_Sodium_Compat::crypto_sign_detached(), but produces
     * the same result.
     *
     * @param string $filePath  Absolute path to a file on the filesystem
     * @param string $secretKey Secret signing key
     *
     * @return string           Ed25519 signature
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign($filePath, $secretKey)
    {
        /* Type checks: */
        if (!is_string($filePath)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($filePath) . ' given.');
        }
        if (!is_string($secretKey)) {
            throw new TypeError('Argument 2 must be a string, ' . gettype($secretKey) . ' given.');
        }

        /* Input validation: */
        if (self::strlen($secretKey) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_SECRETKEYBYTES) {
            throw new TypeError('Argument 2 must be CRYPTO_SIGN_SECRETKEYBYTES bytes');
        }
        if (PHP_INT_SIZE === 4) {
            return self::sign_core32($filePath, $secretKey);
        }

        /** @var int $size */
        $size = filesize($filePath);
        if (!is_int($size)) {
            throw new SodiumException('Could not obtain the file size');
        }

        /** @var resource $fp */
        $fp = fopen($filePath, 'rb');
        if (!is_resource($fp)) {
            throw new SodiumException('Could not open input file for reading');
        }

        /** @var string $az */
        $az = hash('sha512', self::substr($secretKey, 0, 32), true);

        $az[0] = self::intToChr(self::chrToInt($az[0]) & 248);
        $az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64);

        $hs = hash_init('sha512');
        self::hash_update($hs, self::substr($az, 32, 32));
        /** @var resource $hs */
        $hs = self::updateHashWithFile($hs, $fp, $size);

        /** @var string $nonceHash */
        $nonceHash = hash_final($hs, true);

        /** @var string $pk */
        $pk = self::substr($secretKey, 32, 32);

        /** @var string $nonce */
        $nonce = ParagonIE_Sodium_Core_Ed25519::sc_reduce($nonceHash) . self::substr($nonceHash, 32);

        /** @var string $sig */
        $sig = ParagonIE_Sodium_Core_Ed25519::ge_p3_tobytes(
            ParagonIE_Sodium_Core_Ed25519::ge_scalarmult_base($nonce)
        );

        $hs = hash_init('sha512');
        self::hash_update($hs, self::substr($sig, 0, 32));
        self::hash_update($hs, self::substr($pk, 0, 32));
        /** @var resource $hs */
        $hs = self::updateHashWithFile($hs, $fp, $size);

        /** @var string $hramHash */
        $hramHash = hash_final($hs, true);

        /** @var string $hram */
        $hram = ParagonIE_Sodium_Core_Ed25519::sc_reduce($hramHash);

        /** @var string $sigAfter */
        $sigAfter = ParagonIE_Sodium_Core_Ed25519::sc_muladd($hram, $az, $nonce);

        /** @var string $sig */
        $sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32);

        try {
            ParagonIE_Sodium_Compat::memzero($az);
        } catch (SodiumException $ex) {
            $az = null;
        }
        fclose($fp);
        return $sig;
    }

    /**
     * Verify a file (rather than a string). Uses less memory than
     * ParagonIE_Sodium_Compat::crypto_sign_verify_detached(), but
     * produces the same result.
     *
     * @param string $sig       Ed25519 signature
     * @param string $filePath  Absolute path to a file on the filesystem
     * @param string $publicKey Signing public key
     *
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     * @throws Exception
     */
    public static function verify($sig, $filePath, $publicKey)
    {
        /* Type checks: */
        if (!is_string($sig)) {
            throw new TypeError('Argument 1 must be a string, ' . gettype($sig) . ' given.');
        }
        if (!is_string($filePath)) {
            throw new TypeError('Argument 2 must be a string, ' . gettype($filePath) . ' given.');
        }
        if (!is_string($publicKey)) {
            throw new TypeError('Argument 3 must be a string, ' . gettype($publicKey) . ' given.');
        }

        /* Input validation: */
        if (self::strlen($sig) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_BYTES) {
            throw new TypeError('Argument 1 must be CRYPTO_SIGN_BYTES bytes');
        }
        if (self::strlen($publicKey) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_PUBLICKEYBYTES) {
            throw new TypeError('Argument 3 must be CRYPTO_SIGN_PUBLICKEYBYTES bytes');
        }
        if (self::strlen($sig) < 64) {
            throw new SodiumException('Signature is too short');
        }

        if (PHP_INT_SIZE === 4) {
            return self::verify_core32($sig, $filePath, $publicKey);
        }

        /* Security checks */
        if (
            (ParagonIE_Sodium_Core_Ed25519::chrToInt($sig[63]) & 240)
                &&
            ParagonIE_Sodium_Core_Ed25519::check_S_lt_L(self::substr($sig, 32, 32))
        ) {
            throw new SodiumException('S < L - Invalid signature');
        }
        if (ParagonIE_Sodium_Core_Ed25519::small_order($sig)) {
            throw new SodiumException('Signature is on too small of an order');
        }
        if ((self::chrToInt($sig[63]) & 224) !== 0) {
            throw new SodiumException('Invalid signature');
        }
        $d = 0;
        for ($i = 0; $i < 32; ++$i) {
            $d |= self::chrToInt($publicKey[$i]);
        }
        if ($d === 0) {
            throw new SodiumException('All zero public key');
        }

        /** @var int $size */
        $size = filesize($filePath);
        if (!is_int($size)) {
            throw new SodiumException('Could not obtain the file size');
        }

        /** @var resource $fp */
        $fp = fopen($filePath, 'rb');
        if (!is_resource($fp)) {
            throw new SodiumException('Could not open input file for reading');
        }

        /** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */
        $orig = ParagonIE_Sodium_Compat::$fastMult;

        // Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification.
        ParagonIE_Sodium_Compat::$fastMult = true;

        /** @var ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A */
        $A = ParagonIE_Sodium_Core_Ed25519::ge_frombytes_negate_vartime($publicKey);

        $hs = hash_init('sha512');
        self::hash_update($hs, self::substr($sig, 0, 32));
        self::hash_update($hs, self::substr($publicKey, 0, 32));
        /** @var resource $hs */
        $hs = self::updateHashWithFile($hs, $fp, $size);
        /** @var string $hDigest */
        $hDigest = hash_final($hs, true);

        /** @var string $h */
        $h = ParagonIE_Sodium_Core_Ed25519::sc_reduce($hDigest) . self::substr($hDigest, 32);

        /** @var ParagonIE_Sodium_Core_Curve25519_Ge_P2 $R */
        $R = ParagonIE_Sodium_Core_Ed25519::ge_double_scalarmult_vartime(
            $h,
            $A,
            self::substr($sig, 32)
        );

        /** @var string $rcheck */
        $rcheck = ParagonIE_Sodium_Core_Ed25519::ge_tobytes($R);

        // Close the file handle
        fclose($fp);

        // Reset ParagonIE_Sodium_Compat::$fastMult to what it was before.
        ParagonIE_Sodium_Compat::$fastMult = $orig;
        return self::verify_32($rcheck, self::substr($sig, 0, 32));
    }

    /**
     * @param resource $ifp
     * @param resource $ofp
     * @param int      $mlen
     * @param string   $nonce
     * @param string   $boxKeypair
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function box_encrypt($ifp, $ofp, $mlen, $nonce, $boxKeypair)
    {
        if (PHP_INT_SIZE === 4) {
            return self::secretbox_encrypt(
                $ifp,
                $ofp,
                $mlen,
                $nonce,
                ParagonIE_Sodium_Crypto32::box_beforenm(
                    ParagonIE_Sodium_Crypto32::box_secretkey($boxKeypair),
                    ParagonIE_Sodium_Crypto32::box_publickey($boxKeypair)
                )
            );
        }
        return self::secretbox_encrypt(
            $ifp,
            $ofp,
            $mlen,
            $nonce,
            ParagonIE_Sodium_Crypto::box_beforenm(
                ParagonIE_Sodium_Crypto::box_secretkey($boxKeypair),
                ParagonIE_Sodium_Crypto::box_publickey($boxKeypair)
            )
        );
    }


    /**
     * @param resource $ifp
     * @param resource $ofp
     * @param int      $mlen
     * @param string   $nonce
     * @param string   $boxKeypair
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function box_decrypt($ifp, $ofp, $mlen, $nonce, $boxKeypair)
    {
        if (PHP_INT_SIZE === 4) {
            return self::secretbox_decrypt(
                $ifp,
                $ofp,
                $mlen,
                $nonce,
                ParagonIE_Sodium_Crypto32::box_beforenm(
                    ParagonIE_Sodium_Crypto32::box_secretkey($boxKeypair),
                    ParagonIE_Sodium_Crypto32::box_publickey($boxKeypair)
                )
            );
        }
        return self::secretbox_decrypt(
            $ifp,
            $ofp,
            $mlen,
            $nonce,
            ParagonIE_Sodium_Crypto::box_beforenm(
                ParagonIE_Sodium_Crypto::box_secretkey($boxKeypair),
                ParagonIE_Sodium_Crypto::box_publickey($boxKeypair)
            )
        );
    }

    /**
     * Encrypt a file
     *
     * @param resource $ifp
     * @param resource $ofp
     * @param int $mlen
     * @param string $nonce
     * @param string $key
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function secretbox_encrypt($ifp, $ofp, $mlen, $nonce, $key)
    {
        if (PHP_INT_SIZE === 4) {
            return self::secretbox_encrypt_core32($ifp, $ofp, $mlen, $nonce, $key);
        }

        $plaintext = fread($ifp, 32);
        if (!is_string($plaintext)) {
            throw new SodiumException('Could not read input file');
        }
        $first32 = self::ftell($ifp);

        /** @var string $subkey */
        $subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key);

        /** @var string $realNonce */
        $realNonce = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);

        /** @var string $block0 */
        $block0 = str_repeat("\x00", 32);

        /** @var int $mlen - Length of the plaintext message */
        $mlen0 = $mlen;
        if ($mlen0 > 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES) {
            $mlen0 = 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES;
        }
        $block0 .= ParagonIE_Sodium_Core_Util::substr($plaintext, 0, $mlen0);

        /** @var string $block0 */
        $block0 = ParagonIE_Sodium_Core_Salsa20::salsa20_xor(
            $block0,
            $realNonce,
            $subkey
        );

        $state = new ParagonIE_Sodium_Core_Poly1305_State(
            ParagonIE_Sodium_Core_Util::substr(
                $block0,
                0,
                ParagonIE_Sodium_Crypto::onetimeauth_poly1305_KEYBYTES
            )
        );

        // Pre-write 16 blank bytes for the Poly1305 tag
        $start = self::ftell($ofp);
        fwrite($ofp, str_repeat("\x00", 16));

        /** @var string $c */
        $cBlock = ParagonIE_Sodium_Core_Util::substr(
            $block0,
            ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES
        );
        $state->update($cBlock);
        fwrite($ofp, $cBlock);
        $mlen -= 32;

        /** @var int $iter */
        $iter = 1;

        /** @var int $incr */
        $incr = self::BUFFER_SIZE >> 6;

        /*
         * Set the cursor to the end of the first half-block. All future bytes will
         * generated from salsa20_xor_ic, starting from 1 (second block).
         */
        fseek($ifp, $first32, SEEK_SET);

        while ($mlen > 0) {
            $blockSize = $mlen > self::BUFFER_SIZE
                ? self::BUFFER_SIZE
                : $mlen;
            $plaintext = fread($ifp, $blockSize);
            if (!is_string($plaintext)) {
                throw new SodiumException('Could not read input file');
            }
            $cBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
                $plaintext,
                $realNonce,
                $iter,
                $subkey
            );
            fwrite($ofp, $cBlock, $blockSize);
            $state->update($cBlock);

            $mlen -= $blockSize;
            $iter += $incr;
        }
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
            ParagonIE_Sodium_Compat::memzero($subkey);
        } catch (SodiumException $ex) {
            $block0 = null;
            $subkey = null;
        }
        $end = self::ftell($ofp);

        /*
         * Write the Poly1305 authentication tag that provides integrity
         * over the ciphertext (encrypt-then-MAC)
         */
        fseek($ofp, $start, SEEK_SET);
        fwrite($ofp, $state->finish(), ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_MACBYTES);
        fseek($ofp, $end, SEEK_SET);
        unset($state);

        return true;
    }

    /**
     * Decrypt a file
     *
     * @param resource $ifp
     * @param resource $ofp
     * @param int $mlen
     * @param string $nonce
     * @param string $key
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function secretbox_decrypt($ifp, $ofp, $mlen, $nonce, $key)
    {
        if (PHP_INT_SIZE === 4) {
            return self::secretbox_decrypt_core32($ifp, $ofp, $mlen, $nonce, $key);
        }
        $tag = fread($ifp, 16);
        if (!is_string($tag)) {
            throw new SodiumException('Could not read input file');
        }

        /** @var string $subkey */
        $subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key);

        /** @var string $realNonce */
        $realNonce = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);

        /** @var string $block0 */
        $block0 = ParagonIE_Sodium_Core_Salsa20::salsa20(
            64,
            ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8),
            $subkey
        );

        /* Verify the Poly1305 MAC -before- attempting to decrypt! */
        $state = new ParagonIE_Sodium_Core_Poly1305_State(self::substr($block0, 0, 32));
        if (!self::onetimeauth_verify($state, $ifp, $tag, $mlen)) {
            throw new SodiumException('Invalid MAC');
        }

        /*
         * Set the cursor to the end of the first half-block. All future bytes will
         * generated from salsa20_xor_ic, starting from 1 (second block).
         */
        $first32 = fread($ifp, 32);
        if (!is_string($first32)) {
            throw new SodiumException('Could not read input file');
        }
        $first32len = self::strlen($first32);
        fwrite(
            $ofp,
            self::xorStrings(
                self::substr($block0, 32, $first32len),
                self::substr($first32, 0, $first32len)
            )
        );
        $mlen -= 32;

        /** @var int $iter */
        $iter = 1;

        /** @var int $incr */
        $incr = self::BUFFER_SIZE >> 6;

        /* Decrypts ciphertext, writes to output file. */
        while ($mlen > 0) {
            $blockSize = $mlen > self::BUFFER_SIZE
                ? self::BUFFER_SIZE
                : $mlen;
            $ciphertext = fread($ifp, $blockSize);
            if (!is_string($ciphertext)) {
                throw new SodiumException('Could not read input file');
            }
            $pBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
                $ciphertext,
                $realNonce,
                $iter,
                $subkey
            );
            fwrite($ofp, $pBlock, $blockSize);
            $mlen -= $blockSize;
            $iter += $incr;
        }
        return true;
    }

    /**
     * @param ParagonIE_Sodium_Core_Poly1305_State $state
     * @param resource $ifp
     * @param string $tag
     * @param int $mlen
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function onetimeauth_verify(
        ParagonIE_Sodium_Core_Poly1305_State $state,
        $ifp,
        $tag = '',
        $mlen = 0
    ) {
        /** @var int $pos */
        $pos = self::ftell($ifp);

        /** @var int $iter */
        $iter = 1;

        /** @var int $incr */
        $incr = self::BUFFER_SIZE >> 6;

        while ($mlen > 0) {
            $blockSize = $mlen > self::BUFFER_SIZE
                ? self::BUFFER_SIZE
                : $mlen;
            $ciphertext = fread($ifp, $blockSize);
            if (!is_string($ciphertext)) {
                throw new SodiumException('Could not read input file');
            }
            $state->update($ciphertext);
            $mlen -= $blockSize;
            $iter += $incr;
        }
        $res = ParagonIE_Sodium_Core_Util::verify_16($tag, $state->finish());

        fseek($ifp, $pos, SEEK_SET);
        return $res;
    }

    /**
     * Update a hash context with the contents of a file, without
     * loading the entire file into memory.
     *
     * @param resource|HashContext $hash
     * @param resource $fp
     * @param int $size
     * @return resource|object Resource on PHP < 7.2, HashContext object on PHP >= 7.2
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress PossiblyInvalidArgument
     *                 PHP 7.2 changes from a resource to an object,
     *                 which causes Psalm to complain about an error.
     * @psalm-suppress TypeCoercion
     *                 Ditto.
     */
    public static function updateHashWithFile($hash, $fp, $size = 0)
    {
        /* Type checks: */
        if (PHP_VERSION_ID < 70200) {
            if (!is_resource($hash)) {
                throw new TypeError('Argument 1 must be a resource, ' . gettype($hash) . ' given.');
            }
        } else {
            if (!is_object($hash)) {
                throw new TypeError('Argument 1 must be an object (PHP 7.2+), ' . gettype($hash) . ' given.');
            }
        }

        if (!is_resource($fp)) {
            throw new TypeError('Argument 2 must be a resource, ' . gettype($fp) . ' given.');
        }
        if (!is_int($size)) {
            throw new TypeError('Argument 3 must be an integer, ' . gettype($size) . ' given.');
        }

        /** @var int $originalPosition */
        $originalPosition = self::ftell($fp);

        // Move file pointer to beginning of file
        fseek($fp, 0, SEEK_SET);
        for ($i = 0; $i < $size; $i += self::BUFFER_SIZE) {
            /** @var string|bool $message */
            $message = fread(
                $fp,
                ($size - $i) > self::BUFFER_SIZE
                    ? $size - $i
                    : self::BUFFER_SIZE
            );
            if (!is_string($message)) {
                throw new SodiumException('Unexpected error reading from file.');
            }
            /** @var string $message */
            /** @psalm-suppress InvalidArgument */
            self::hash_update($hash, $message);
        }
        // Reset file pointer's position
        fseek($fp, $originalPosition, SEEK_SET);
        return $hash;
    }

    /**
     * Sign a file (rather than a string). Uses less memory than
     * ParagonIE_Sodium_Compat::crypto_sign_detached(), but produces
     * the same result. (32-bit)
     *
     * @param string $filePath  Absolute path to a file on the filesystem
     * @param string $secretKey Secret signing key
     *
     * @return string           Ed25519 signature
     * @throws SodiumException
     * @throws TypeError
     */
    private static function sign_core32($filePath, $secretKey)
    {
        /** @var int|bool $size */
        $size = filesize($filePath);
        if (!is_int($size)) {
            throw new SodiumException('Could not obtain the file size');
        }
        /** @var int $size */

        /** @var resource|bool $fp */
        $fp = fopen($filePath, 'rb');
        if (!is_resource($fp)) {
            throw new SodiumException('Could not open input file for reading');
        }
        /** @var resource $fp */

        /** @var string $az */
        $az = hash('sha512', self::substr($secretKey, 0, 32), true);

        $az[0] = self::intToChr(self::chrToInt($az[0]) & 248);
        $az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64);

        $hs = hash_init('sha512');
        self::hash_update($hs, self::substr($az, 32, 32));
        /** @var resource $hs */
        $hs = self::updateHashWithFile($hs, $fp, $size);

        /** @var string $nonceHash */
        $nonceHash = hash_final($hs, true);

        /** @var string $pk */
        $pk = self::substr($secretKey, 32, 32);

        /** @var string $nonce */
        $nonce = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($nonceHash) . self::substr($nonceHash, 32);

        /** @var string $sig */
        $sig = ParagonIE_Sodium_Core32_Ed25519::ge_p3_tobytes(
            ParagonIE_Sodium_Core32_Ed25519::ge_scalarmult_base($nonce)
        );

        $hs = hash_init('sha512');
        self::hash_update($hs, self::substr($sig, 0, 32));
        self::hash_update($hs, self::substr($pk, 0, 32));
        /** @var resource $hs */
        $hs = self::updateHashWithFile($hs, $fp, $size);

        /** @var string $hramHash */
        $hramHash = hash_final($hs, true);

        /** @var string $hram */
        $hram = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($hramHash);

        /** @var string $sigAfter */
        $sigAfter = ParagonIE_Sodium_Core32_Ed25519::sc_muladd($hram, $az, $nonce);

        /** @var string $sig */
        $sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32);

        try {
            ParagonIE_Sodium_Compat::memzero($az);
        } catch (SodiumException $ex) {
            $az = null;
        }
        fclose($fp);
        return $sig;
    }

    /**
     *
     * Verify a file (rather than a string). Uses less memory than
     * ParagonIE_Sodium_Compat::crypto_sign_verify_detached(), but
     * produces the same result. (32-bit)
     *
     * @param string $sig       Ed25519 signature
     * @param string $filePath  Absolute path to a file on the filesystem
     * @param string $publicKey Signing public key
     *
     * @return bool
     * @throws SodiumException
     * @throws Exception
     */
    public static function verify_core32($sig, $filePath, $publicKey)
    {
        /* Security checks */
        if (ParagonIE_Sodium_Core32_Ed25519::check_S_lt_L(self::substr($sig, 32, 32))) {
            throw new SodiumException('S < L - Invalid signature');
        }
        if (ParagonIE_Sodium_Core32_Ed25519::small_order($sig)) {
            throw new SodiumException('Signature is on too small of an order');
        }
        if ((self::chrToInt($sig[63]) & 224) !== 0) {
            throw new SodiumException('Invalid signature');
        }
        $d = 0;
        for ($i = 0; $i < 32; ++$i) {
            $d |= self::chrToInt($publicKey[$i]);
        }
        if ($d === 0) {
            throw new SodiumException('All zero public key');
        }

        /** @var int|bool $size */
        $size = filesize($filePath);
        if (!is_int($size)) {
            throw new SodiumException('Could not obtain the file size');
        }
        /** @var int $size */

        /** @var resource|bool $fp */
        $fp = fopen($filePath, 'rb');
        if (!is_resource($fp)) {
            throw new SodiumException('Could not open input file for reading');
        }
        /** @var resource $fp */

        /** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */
        $orig = ParagonIE_Sodium_Compat::$fastMult;

        // Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification.
        ParagonIE_Sodium_Compat::$fastMult = true;

        /** @var ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $A */
        $A = ParagonIE_Sodium_Core32_Ed25519::ge_frombytes_negate_vartime($publicKey);

        $hs = hash_init('sha512');
        self::hash_update($hs, self::substr($sig, 0, 32));
        self::hash_update($hs, self::substr($publicKey, 0, 32));
        /** @var resource $hs */
        $hs = self::updateHashWithFile($hs, $fp, $size);
        /** @var string $hDigest */
        $hDigest = hash_final($hs, true);

        /** @var string $h */
        $h = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($hDigest) . self::substr($hDigest, 32);

        /** @var ParagonIE_Sodium_Core32_Curve25519_Ge_P2 $R */
        $R = ParagonIE_Sodium_Core32_Ed25519::ge_double_scalarmult_vartime(
            $h,
            $A,
            self::substr($sig, 32)
        );

        /** @var string $rcheck */
        $rcheck = ParagonIE_Sodium_Core32_Ed25519::ge_tobytes($R);

        // Close the file handle
        fclose($fp);

        // Reset ParagonIE_Sodium_Compat::$fastMult to what it was before.
        ParagonIE_Sodium_Compat::$fastMult = $orig;
        return self::verify_32($rcheck, self::substr($sig, 0, 32));
    }

    /**
     * Encrypt a file (32-bit)
     *
     * @param resource $ifp
     * @param resource $ofp
     * @param int $mlen
     * @param string $nonce
     * @param string $key
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function secretbox_encrypt_core32($ifp, $ofp, $mlen, $nonce, $key)
    {
        $plaintext = fread($ifp, 32);
        if (!is_string($plaintext)) {
            throw new SodiumException('Could not read input file');
        }
        $first32 = self::ftell($ifp);

        /** @var string $subkey */
        $subkey = ParagonIE_Sodium_Core32_HSalsa20::hsalsa20($nonce, $key);

        /** @var string $realNonce */
        $realNonce = ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8);

        /** @var string $block0 */
        $block0 = str_repeat("\x00", 32);

        /** @var int $mlen - Length of the plaintext message */
        $mlen0 = $mlen;
        if ($mlen0 > 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES) {
            $mlen0 = 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES;
        }
        $block0 .= ParagonIE_Sodium_Core32_Util::substr($plaintext, 0, $mlen0);

        /** @var string $block0 */
        $block0 = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor(
            $block0,
            $realNonce,
            $subkey
        );

        $state = new ParagonIE_Sodium_Core32_Poly1305_State(
            ParagonIE_Sodium_Core32_Util::substr(
                $block0,
                0,
                ParagonIE_Sodium_Crypto::onetimeauth_poly1305_KEYBYTES
            )
        );

        // Pre-write 16 blank bytes for the Poly1305 tag
        $start = self::ftell($ofp);
        fwrite($ofp, str_repeat("\x00", 16));

        /** @var string $c */
        $cBlock = ParagonIE_Sodium_Core32_Util::substr(
            $block0,
            ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES
        );
        $state->update($cBlock);
        fwrite($ofp, $cBlock);
        $mlen -= 32;

        /** @var int $iter */
        $iter = 1;

        /** @var int $incr */
        $incr = self::BUFFER_SIZE >> 6;

        /*
         * Set the cursor to the end of the first half-block. All future bytes will
         * generated from salsa20_xor_ic, starting from 1 (second block).
         */
        fseek($ifp, $first32, SEEK_SET);

        while ($mlen > 0) {
            $blockSize = $mlen > self::BUFFER_SIZE
                ? self::BUFFER_SIZE
                : $mlen;
            $plaintext = fread($ifp, $blockSize);
            if (!is_string($plaintext)) {
                throw new SodiumException('Could not read input file');
            }
            $cBlock = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic(
                $plaintext,
                $realNonce,
                $iter,
                $subkey
            );
            fwrite($ofp, $cBlock, $blockSize);
            $state->update($cBlock);

            $mlen -= $blockSize;
            $iter += $incr;
        }
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
            ParagonIE_Sodium_Compat::memzero($subkey);
        } catch (SodiumException $ex) {
            $block0 = null;
            $subkey = null;
        }
        $end = self::ftell($ofp);

        /*
         * Write the Poly1305 authentication tag that provides integrity
         * over the ciphertext (encrypt-then-MAC)
         */
        fseek($ofp, $start, SEEK_SET);
        fwrite($ofp, $state->finish(), ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_MACBYTES);
        fseek($ofp, $end, SEEK_SET);
        unset($state);

        return true;
    }

    /**
     * Decrypt a file (32-bit)
     *
     * @param resource $ifp
     * @param resource $ofp
     * @param int $mlen
     * @param string $nonce
     * @param string $key
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function secretbox_decrypt_core32($ifp, $ofp, $mlen, $nonce, $key)
    {
        $tag = fread($ifp, 16);
        if (!is_string($tag)) {
            throw new SodiumException('Could not read input file');
        }

        /** @var string $subkey */
        $subkey = ParagonIE_Sodium_Core32_HSalsa20::hsalsa20($nonce, $key);

        /** @var string $realNonce */
        $realNonce = ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8);

        /** @var string $block0 */
        $block0 = ParagonIE_Sodium_Core32_Salsa20::salsa20(
            64,
            ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8),
            $subkey
        );

        /* Verify the Poly1305 MAC -before- attempting to decrypt! */
        $state = new ParagonIE_Sodium_Core32_Poly1305_State(self::substr($block0, 0, 32));
        if (!self::onetimeauth_verify_core32($state, $ifp, $tag, $mlen)) {
            throw new SodiumException('Invalid MAC');
        }

        /*
         * Set the cursor to the end of the first half-block. All future bytes will
         * generated from salsa20_xor_ic, starting from 1 (second block).
         */
        $first32 = fread($ifp, 32);
        if (!is_string($first32)) {
            throw new SodiumException('Could not read input file');
        }
        $first32len = self::strlen($first32);
        fwrite(
            $ofp,
            self::xorStrings(
                self::substr($block0, 32, $first32len),
                self::substr($first32, 0, $first32len)
            )
        );
        $mlen -= 32;

        /** @var int $iter */
        $iter = 1;

        /** @var int $incr */
        $incr = self::BUFFER_SIZE >> 6;

        /* Decrypts ciphertext, writes to output file. */
        while ($mlen > 0) {
            $blockSize = $mlen > self::BUFFER_SIZE
                ? self::BUFFER_SIZE
                : $mlen;
            $ciphertext = fread($ifp, $blockSize);
            if (!is_string($ciphertext)) {
                throw new SodiumException('Could not read input file');
            }
            $pBlock = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic(
                $ciphertext,
                $realNonce,
                $iter,
                $subkey
            );
            fwrite($ofp, $pBlock, $blockSize);
            $mlen -= $blockSize;
            $iter += $incr;
        }
        return true;
    }

    /**
     * One-time message authentication for 32-bit systems
     *
     * @param ParagonIE_Sodium_Core32_Poly1305_State $state
     * @param resource $ifp
     * @param string $tag
     * @param int $mlen
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function onetimeauth_verify_core32(
        ParagonIE_Sodium_Core32_Poly1305_State $state,
        $ifp,
        $tag = '',
        $mlen = 0
    ) {
        /** @var int $pos */
        $pos = self::ftell($ifp);

        while ($mlen > 0) {
            $blockSize = $mlen > self::BUFFER_SIZE
                ? self::BUFFER_SIZE
                : $mlen;
            $ciphertext = fread($ifp, $blockSize);
            if (!is_string($ciphertext)) {
                throw new SodiumException('Could not read input file');
            }
            $state->update($ciphertext);
            $mlen -= $blockSize;
        }
        $res = ParagonIE_Sodium_Core32_Util::verify_16($tag, $state->finish());

        fseek($ifp, $pos, SEEK_SET);
        return $res;
    }

    /**
     * @param resource $resource
     * @return int
     * @throws SodiumException
     */
    private static function ftell($resource)
    {
        $return = ftell($resource);
        if (!is_int($return)) {
            throw new SodiumException('ftell() returned false');
        }
        return (int) $return;
    }
}
Core32/Curve25519.php000064400000407142152331075510010016 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_Curve25519', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_Curve25519
 *
 * Implements Curve25519 core functions
 *
 * Based on the ref10 curve25519 code provided by libsodium
 *
 * @ref https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_core/curve25519/ref10/curve25519_ref10.c
 */
abstract class ParagonIE_Sodium_Core32_Curve25519 extends ParagonIE_Sodium_Core32_Curve25519_H
{
    /**
     * Get a field element of size 10 with a value of 0
     *
     * @internal You should not use this directly from another application
     *
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fe_0()
    {
        return ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
            array(
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32()
            )
        );
    }

    /**
     * Get a field element of size 10 with a value of 1
     *
     * @internal You should not use this directly from another application
     *
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fe_1()
    {
        return ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
            array(
                ParagonIE_Sodium_Core32_Int32::fromInt(1),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32(),
                new ParagonIE_Sodium_Core32_Int32()
            )
        );
    }

    /**
     * Add two field elements.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $f
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $g
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedMethodCall
     */
    public static function fe_add(
        ParagonIE_Sodium_Core32_Curve25519_Fe $f,
        ParagonIE_Sodium_Core32_Curve25519_Fe $g
    ) {
        $arr = array();
        for ($i = 0; $i < 10; ++$i) {
            $arr[$i] = $f[$i]->addInt32($g[$i]);
        }
        /** @var array<int, ParagonIE_Sodium_Core32_Int32> $arr */
        return ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray($arr);
    }

    /**
     * Constant-time conditional move.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $f
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $g
     * @param int $b
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedMethodCall
     */
    public static function fe_cmov(
        ParagonIE_Sodium_Core32_Curve25519_Fe $f,
        ParagonIE_Sodium_Core32_Curve25519_Fe $g,
        $b = 0
    ) {
        /** @var array<int, ParagonIE_Sodium_Core32_Int32> $h */
        $h = array();
        for ($i = 0; $i < 10; ++$i) {
            if (!($f[$i] instanceof ParagonIE_Sodium_Core32_Int32)) {
                throw new TypeError('Expected Int32');
            }
            if (!($g[$i] instanceof ParagonIE_Sodium_Core32_Int32)) {
                throw new TypeError('Expected Int32');
            }
            $h[$i] = $f[$i]->xorInt32(
                $f[$i]->xorInt32($g[$i])->mask($b)
            );
        }
        /** @var array<int, ParagonIE_Sodium_Core32_Int32> $h */
        return ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray($h);
    }

    /**
     * Create a copy of a field element.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $f
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public static function fe_copy(ParagonIE_Sodium_Core32_Curve25519_Fe $f)
    {
        $h = clone $f;
        return $h;
    }

    /**
     * Give: 32-byte string.
     * Receive: A field element object to use for internal calculations.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $s
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws RangeException
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedMethodCall
     */
    public static function fe_frombytes($s)
    {
        if (self::strlen($s) !== 32) {
            throw new RangeException('Expected a 32-byte string.');
        }
        /** @var ParagonIE_Sodium_Core32_Int32 $h0 */
        $h0 = ParagonIE_Sodium_Core32_Int32::fromInt(
            self::load_4($s)
        );
        /** @var ParagonIE_Sodium_Core32_Int32 $h1 */
        $h1 = ParagonIE_Sodium_Core32_Int32::fromInt(
            self::load_3(self::substr($s, 4, 3)) << 6
        );
        /** @var ParagonIE_Sodium_Core32_Int32 $h2 */
        $h2 = ParagonIE_Sodium_Core32_Int32::fromInt(
            self::load_3(self::substr($s, 7, 3)) << 5
        );
        /** @var ParagonIE_Sodium_Core32_Int32 $h3 */
        $h3 = ParagonIE_Sodium_Core32_Int32::fromInt(
            self::load_3(self::substr($s, 10, 3)) << 3
        );
        /** @var ParagonIE_Sodium_Core32_Int32 $h4 */
        $h4 = ParagonIE_Sodium_Core32_Int32::fromInt(
            self::load_3(self::substr($s, 13, 3)) << 2
        );
        /** @var ParagonIE_Sodium_Core32_Int32 $h5 */
        $h5 = ParagonIE_Sodium_Core32_Int32::fromInt(
            self::load_4(self::substr($s, 16, 4))
        );
        /** @var ParagonIE_Sodium_Core32_Int32 $h6 */
        $h6 = ParagonIE_Sodium_Core32_Int32::fromInt(
            self::load_3(self::substr($s, 20, 3)) << 7
        );
        /** @var ParagonIE_Sodium_Core32_Int32 $h7 */
        $h7 = ParagonIE_Sodium_Core32_Int32::fromInt(
            self::load_3(self::substr($s, 23, 3)) << 5
        );
        /** @var ParagonIE_Sodium_Core32_Int32 $h8 */
        $h8 = ParagonIE_Sodium_Core32_Int32::fromInt(
            self::load_3(self::substr($s, 26, 3)) << 4
        );
        /** @var ParagonIE_Sodium_Core32_Int32 $h9 */
        $h9 = ParagonIE_Sodium_Core32_Int32::fromInt(
            (self::load_3(self::substr($s, 29, 3)) & 8388607) << 2
        );

        $carry9 = $h9->addInt(1 << 24)->shiftRight(25);
        $h0 = $h0->addInt32($carry9->mulInt(19, 5));
        $h9 = $h9->subInt32($carry9->shiftLeft(25));

        $carry1 = $h1->addInt(1 << 24)->shiftRight(25);
        $h2 = $h2->addInt32($carry1);
        $h1 = $h1->subInt32($carry1->shiftLeft(25));

        $carry3 = $h3->addInt(1 << 24)->shiftRight(25);
        $h4 = $h4->addInt32($carry3);
        $h3 = $h3->subInt32($carry3->shiftLeft(25));

        $carry5 = $h5->addInt(1 << 24)->shiftRight(25);
        $h6 = $h6->addInt32($carry5);
        $h5 = $h5->subInt32($carry5->shiftLeft(25));

        $carry7 = $h7->addInt(1 << 24)->shiftRight(25);
        $h8 = $h8->addInt32($carry7);
        $h7 = $h7->subInt32($carry7->shiftLeft(25));

        $carry0 = $h0->addInt(1 << 25)->shiftRight(26);
        $h1 = $h1->addInt32($carry0);
        $h0 = $h0->subInt32($carry0->shiftLeft(26));

        $carry2 = $h2->addInt(1 << 25)->shiftRight(26);
        $h3 = $h3->addInt32($carry2);
        $h2 = $h2->subInt32($carry2->shiftLeft(26));

        $carry4 = $h4->addInt(1 << 25)->shiftRight(26);
        $h5 = $h5->addInt32($carry4);
        $h4 = $h4->subInt32($carry4->shiftLeft(26));

        $carry6 = $h6->addInt(1 << 25)->shiftRight(26);
        $h7 = $h7->addInt32($carry6);
        $h6 = $h6->subInt32($carry6->shiftLeft(26));

        $carry8 = $h8->addInt(1 << 25)->shiftRight(26);
        $h9 = $h9->addInt32($carry8);
        $h8 = $h8->subInt32($carry8->shiftLeft(26));

        return ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
            array($h0, $h1, $h2,$h3, $h4, $h5, $h6, $h7, $h8, $h9)
        );
    }

    /**
     * Convert a field element to a byte string.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $h
     * @return string
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedMethodCall
     */
    public static function fe_tobytes(ParagonIE_Sodium_Core32_Curve25519_Fe $h)
    {
        /**
         * @var ParagonIE_Sodium_Core32_Int64[] $f
         * @var ParagonIE_Sodium_Core32_Int64 $q
         */
        $f = array();

        for ($i = 0; $i < 10; ++$i) {
            $f[$i] = $h[$i]->toInt64();
        }

        $q = $f[9]->mulInt(19, 5)->addInt(1 << 14)->shiftRight(25)
            ->addInt64($f[0])->shiftRight(26)
            ->addInt64($f[1])->shiftRight(25)
            ->addInt64($f[2])->shiftRight(26)
            ->addInt64($f[3])->shiftRight(25)
            ->addInt64($f[4])->shiftRight(26)
            ->addInt64($f[5])->shiftRight(25)
            ->addInt64($f[6])->shiftRight(26)
            ->addInt64($f[7])->shiftRight(25)
            ->addInt64($f[8])->shiftRight(26)
            ->addInt64($f[9])->shiftRight(25);

        $f[0] = $f[0]->addInt64($q->mulInt(19, 5));

        $carry0 = $f[0]->shiftRight(26);
        $f[1] = $f[1]->addInt64($carry0);
        $f[0] = $f[0]->subInt64($carry0->shiftLeft(26));

        $carry1 = $f[1]->shiftRight(25);
        $f[2] = $f[2]->addInt64($carry1);
        $f[1] = $f[1]->subInt64($carry1->shiftLeft(25));

        $carry2 = $f[2]->shiftRight(26);
        $f[3] = $f[3]->addInt64($carry2);
        $f[2] = $f[2]->subInt64($carry2->shiftLeft(26));

        $carry3 = $f[3]->shiftRight(25);
        $f[4] = $f[4]->addInt64($carry3);
        $f[3] = $f[3]->subInt64($carry3->shiftLeft(25));

        $carry4 = $f[4]->shiftRight(26);
        $f[5] = $f[5]->addInt64($carry4);
        $f[4] = $f[4]->subInt64($carry4->shiftLeft(26));

        $carry5 = $f[5]->shiftRight(25);
        $f[6] = $f[6]->addInt64($carry5);
        $f[5] = $f[5]->subInt64($carry5->shiftLeft(25));

        $carry6 = $f[6]->shiftRight(26);
        $f[7] = $f[7]->addInt64($carry6);
        $f[6] = $f[6]->subInt64($carry6->shiftLeft(26));

        $carry7 = $f[7]->shiftRight(25);
        $f[8] = $f[8]->addInt64($carry7);
        $f[7] = $f[7]->subInt64($carry7->shiftLeft(25));

        $carry8 = $f[8]->shiftRight(26);
        $f[9] = $f[9]->addInt64($carry8);
        $f[8] = $f[8]->subInt64($carry8->shiftLeft(26));

        $carry9 = $f[9]->shiftRight(25);
        $f[9] = $f[9]->subInt64($carry9->shiftLeft(25));

        /** @var int $h0 */
        $h0 = $f[0]->toInt32()->toInt();
        /** @var int $h1 */
        $h1 = $f[1]->toInt32()->toInt();
        /** @var int $h2 */
        $h2 = $f[2]->toInt32()->toInt();
        /** @var int $h3 */
        $h3 = $f[3]->toInt32()->toInt();
        /** @var int $h4 */
        $h4 = $f[4]->toInt32()->toInt();
        /** @var int $h5 */
        $h5 = $f[5]->toInt32()->toInt();
        /** @var int $h6 */
        $h6 = $f[6]->toInt32()->toInt();
        /** @var int $h7 */
        $h7 = $f[7]->toInt32()->toInt();
        /** @var int $h8 */
        $h8 = $f[8]->toInt32()->toInt();
        /** @var int $h9 */
        $h9 = $f[9]->toInt32()->toInt();

        /**
         * @var array<int, int>
         */
        $s = array(
            (int) (($h0 >> 0) & 0xff),
            (int) (($h0 >> 8) & 0xff),
            (int) (($h0 >> 16) & 0xff),
            (int) ((($h0 >> 24) | ($h1 << 2)) & 0xff),
            (int) (($h1 >> 6) & 0xff),
            (int) (($h1 >> 14) & 0xff),
            (int) ((($h1 >> 22) | ($h2 << 3)) & 0xff),
            (int) (($h2 >> 5) & 0xff),
            (int) (($h2 >> 13) & 0xff),
            (int) ((($h2 >> 21) | ($h3 << 5)) & 0xff),
            (int) (($h3 >> 3) & 0xff),
            (int) (($h3 >> 11) & 0xff),
            (int) ((($h3 >> 19) | ($h4 << 6)) & 0xff),
            (int) (($h4 >> 2) & 0xff),
            (int) (($h4 >> 10) & 0xff),
            (int) (($h4 >> 18) & 0xff),
            (int) (($h5 >> 0) & 0xff),
            (int) (($h5 >> 8) & 0xff),
            (int) (($h5 >> 16) & 0xff),
            (int) ((($h5 >> 24) | ($h6 << 1)) & 0xff),
            (int) (($h6 >> 7) & 0xff),
            (int) (($h6 >> 15) & 0xff),
            (int) ((($h6 >> 23) | ($h7 << 3)) & 0xff),
            (int) (($h7 >> 5) & 0xff),
            (int) (($h7 >> 13) & 0xff),
            (int) ((($h7 >> 21) | ($h8 << 4)) & 0xff),
            (int) (($h8 >> 4) & 0xff),
            (int) (($h8 >> 12) & 0xff),
            (int) ((($h8 >> 20) | ($h9 << 6)) & 0xff),
            (int) (($h9 >> 2) & 0xff),
            (int) (($h9 >> 10) & 0xff),
            (int) (($h9 >> 18) & 0xff)
        );
        return self::intArrayToString($s);
    }

    /**
     * Is a field element negative? (1 = yes, 0 = no. Used in calculations.)
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $f
     * @return int
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fe_isnegative(ParagonIE_Sodium_Core32_Curve25519_Fe $f)
    {
        $str = self::fe_tobytes($f);
        return (int) (self::chrToInt($str[0]) & 1);
    }

    /**
     * Returns 0 if this field element results in all NUL bytes.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $f
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fe_isnonzero(ParagonIE_Sodium_Core32_Curve25519_Fe $f)
    {
        static $zero;
        if ($zero === null) {
            $zero = str_repeat("\x00", 32);
        }
        /** @var string $str */
        $str = self::fe_tobytes($f);
        /** @var string $zero */
        return !self::verify_32($str, $zero);
    }

    /**
     * Multiply two field elements
     *
     * h = f * g
     *
     * @internal You should not use this directly from another application
     *
     * @security Is multiplication a source of timing leaks? If so, can we do
     *           anything to prevent that from happening?
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $f
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $g
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fe_mul(
        ParagonIE_Sodium_Core32_Curve25519_Fe $f,
        ParagonIE_Sodium_Core32_Curve25519_Fe $g
    ) {
        /**
         * @var ParagonIE_Sodium_Core32_Int32[] $f
         * @var ParagonIE_Sodium_Core32_Int32[] $g
         * @var ParagonIE_Sodium_Core32_Int64 $f0
         * @var ParagonIE_Sodium_Core32_Int64 $f1
         * @var ParagonIE_Sodium_Core32_Int64 $f2
         * @var ParagonIE_Sodium_Core32_Int64 $f3
         * @var ParagonIE_Sodium_Core32_Int64 $f4
         * @var ParagonIE_Sodium_Core32_Int64 $f5
         * @var ParagonIE_Sodium_Core32_Int64 $f6
         * @var ParagonIE_Sodium_Core32_Int64 $f7
         * @var ParagonIE_Sodium_Core32_Int64 $f8
         * @var ParagonIE_Sodium_Core32_Int64 $f9
         * @var ParagonIE_Sodium_Core32_Int64 $g0
         * @var ParagonIE_Sodium_Core32_Int64 $g1
         * @var ParagonIE_Sodium_Core32_Int64 $g2
         * @var ParagonIE_Sodium_Core32_Int64 $g3
         * @var ParagonIE_Sodium_Core32_Int64 $g4
         * @var ParagonIE_Sodium_Core32_Int64 $g5
         * @var ParagonIE_Sodium_Core32_Int64 $g6
         * @var ParagonIE_Sodium_Core32_Int64 $g7
         * @var ParagonIE_Sodium_Core32_Int64 $g8
         * @var ParagonIE_Sodium_Core32_Int64 $g9
         */
        $f0 = $f[0]->toInt64();
        $f1 = $f[1]->toInt64();
        $f2 = $f[2]->toInt64();
        $f3 = $f[3]->toInt64();
        $f4 = $f[4]->toInt64();
        $f5 = $f[5]->toInt64();
        $f6 = $f[6]->toInt64();
        $f7 = $f[7]->toInt64();
        $f8 = $f[8]->toInt64();
        $f9 = $f[9]->toInt64();
        $g0 = $g[0]->toInt64();
        $g1 = $g[1]->toInt64();
        $g2 = $g[2]->toInt64();
        $g3 = $g[3]->toInt64();
        $g4 = $g[4]->toInt64();
        $g5 = $g[5]->toInt64();
        $g6 = $g[6]->toInt64();
        $g7 = $g[7]->toInt64();
        $g8 = $g[8]->toInt64();
        $g9 = $g[9]->toInt64();
        $g1_19 = $g1->mulInt(19, 5); /* 2^4 <= 19 <= 2^5, but we only want 5 bits */
        $g2_19 = $g2->mulInt(19, 5);
        $g3_19 = $g3->mulInt(19, 5);
        $g4_19 = $g4->mulInt(19, 5);
        $g5_19 = $g5->mulInt(19, 5);
        $g6_19 = $g6->mulInt(19, 5);
        $g7_19 = $g7->mulInt(19, 5);
        $g8_19 = $g8->mulInt(19, 5);
        $g9_19 = $g9->mulInt(19, 5);
        /** @var ParagonIE_Sodium_Core32_Int64 $f1_2 */
        $f1_2 = $f1->shiftLeft(1);
        /** @var ParagonIE_Sodium_Core32_Int64 $f3_2 */
        $f3_2 = $f3->shiftLeft(1);
        /** @var ParagonIE_Sodium_Core32_Int64 $f5_2 */
        $f5_2 = $f5->shiftLeft(1);
        /** @var ParagonIE_Sodium_Core32_Int64 $f7_2 */
        $f7_2 = $f7->shiftLeft(1);
        /** @var ParagonIE_Sodium_Core32_Int64 $f9_2 */
        $f9_2 = $f9->shiftLeft(1);
        $f0g0    = $f0->mulInt64($g0, 27);
        $f0g1    = $f0->mulInt64($g1, 27);
        $f0g2    = $f0->mulInt64($g2, 27);
        $f0g3    = $f0->mulInt64($g3, 27);
        $f0g4    = $f0->mulInt64($g4, 27);
        $f0g5    = $f0->mulInt64($g5, 27);
        $f0g6    = $f0->mulInt64($g6, 27);
        $f0g7    = $f0->mulInt64($g7, 27);
        $f0g8    = $f0->mulInt64($g8, 27);
        $f0g9    = $f0->mulInt64($g9, 27);
        $f1g0    = $f1->mulInt64($g0, 27);
        $f1g1_2  = $f1_2->mulInt64($g1, 27);
        $f1g2    = $f1->mulInt64($g2, 27);
        $f1g3_2  = $f1_2->mulInt64($g3, 27);
        $f1g4    = $f1->mulInt64($g4, 30);
        $f1g5_2  = $f1_2->mulInt64($g5, 30);
        $f1g6    = $f1->mulInt64($g6, 30);
        $f1g7_2  = $f1_2->mulInt64($g7, 30);
        $f1g8    = $f1->mulInt64($g8, 30);
        $f1g9_38 = $g9_19->mulInt64($f1_2, 30);
        $f2g0    = $f2->mulInt64($g0, 30);
        $f2g1    = $f2->mulInt64($g1, 29);
        $f2g2    = $f2->mulInt64($g2, 30);
        $f2g3    = $f2->mulInt64($g3, 29);
        $f2g4    = $f2->mulInt64($g4, 30);
        $f2g5    = $f2->mulInt64($g5, 29);
        $f2g6    = $f2->mulInt64($g6, 30);
        $f2g7    = $f2->mulInt64($g7, 29);
        $f2g8_19 = $g8_19->mulInt64($f2, 30);
        $f2g9_19 = $g9_19->mulInt64($f2, 30);
        $f3g0    = $f3->mulInt64($g0, 30);
        $f3g1_2  = $f3_2->mulInt64($g1, 30);
        $f3g2    = $f3->mulInt64($g2, 30);
        $f3g3_2  = $f3_2->mulInt64($g3, 30);
        $f3g4    = $f3->mulInt64($g4, 30);
        $f3g5_2  = $f3_2->mulInt64($g5, 30);
        $f3g6    = $f3->mulInt64($g6, 30);
        $f3g7_38 = $g7_19->mulInt64($f3_2, 30);
        $f3g8_19 = $g8_19->mulInt64($f3, 30);
        $f3g9_38 = $g9_19->mulInt64($f3_2, 30);
        $f4g0    = $f4->mulInt64($g0, 30);
        $f4g1    = $f4->mulInt64($g1, 30);
        $f4g2    = $f4->mulInt64($g2, 30);
        $f4g3    = $f4->mulInt64($g3, 30);
        $f4g4    = $f4->mulInt64($g4, 30);
        $f4g5    = $f4->mulInt64($g5, 30);
        $f4g6_19 = $g6_19->mulInt64($f4, 30);
        $f4g7_19 = $g7_19->mulInt64($f4, 30);
        $f4g8_19 = $g8_19->mulInt64($f4, 30);
        $f4g9_19 = $g9_19->mulInt64($f4, 30);
        $f5g0    = $f5->mulInt64($g0, 30);
        $f5g1_2  = $f5_2->mulInt64($g1, 30);
        $f5g2    = $f5->mulInt64($g2, 30);
        $f5g3_2  = $f5_2->mulInt64($g3, 30);
        $f5g4    = $f5->mulInt64($g4, 30);
        $f5g5_38 = $g5_19->mulInt64($f5_2, 30);
        $f5g6_19 = $g6_19->mulInt64($f5, 30);
        $f5g7_38 = $g7_19->mulInt64($f5_2, 30);
        $f5g8_19 = $g8_19->mulInt64($f5, 30);
        $f5g9_38 = $g9_19->mulInt64($f5_2, 30);
        $f6g0    = $f6->mulInt64($g0, 30);
        $f6g1    = $f6->mulInt64($g1, 30);
        $f6g2    = $f6->mulInt64($g2, 30);
        $f6g3    = $f6->mulInt64($g3, 30);
        $f6g4_19 = $g4_19->mulInt64($f6, 30);
        $f6g5_19 = $g5_19->mulInt64($f6, 30);
        $f6g6_19 = $g6_19->mulInt64($f6, 30);
        $f6g7_19 = $g7_19->mulInt64($f6, 30);
        $f6g8_19 = $g8_19->mulInt64($f6, 30);
        $f6g9_19 = $g9_19->mulInt64($f6, 30);
        $f7g0    = $f7->mulInt64($g0, 30);
        $f7g1_2  = $g1->mulInt64($f7_2, 30);
        $f7g2    = $f7->mulInt64($g2, 30);
        $f7g3_38 = $g3_19->mulInt64($f7_2, 30);
        $f7g4_19 = $g4_19->mulInt64($f7, 30);
        $f7g5_38 = $g5_19->mulInt64($f7_2, 30);
        $f7g6_19 = $g6_19->mulInt64($f7, 30);
        $f7g7_38 = $g7_19->mulInt64($f7_2, 30);
        $f7g8_19 = $g8_19->mulInt64($f7, 30);
        $f7g9_38 = $g9_19->mulInt64($f7_2, 30);
        $f8g0    = $f8->mulInt64($g0, 30);
        $f8g1    = $f8->mulInt64($g1, 29);
        $f8g2_19 = $g2_19->mulInt64($f8, 30);
        $f8g3_19 = $g3_19->mulInt64($f8, 30);
        $f8g4_19 = $g4_19->mulInt64($f8, 30);
        $f8g5_19 = $g5_19->mulInt64($f8, 30);
        $f8g6_19 = $g6_19->mulInt64($f8, 30);
        $f8g7_19 = $g7_19->mulInt64($f8, 30);
        $f8g8_19 = $g8_19->mulInt64($f8, 30);
        $f8g9_19 = $g9_19->mulInt64($f8, 30);
        $f9g0    = $f9->mulInt64($g0, 30);
        $f9g1_38 = $g1_19->mulInt64($f9_2, 30);
        $f9g2_19 = $g2_19->mulInt64($f9, 30);
        $f9g3_38 = $g3_19->mulInt64($f9_2, 30);
        $f9g4_19 = $g4_19->mulInt64($f9, 30);
        $f9g5_38 = $g5_19->mulInt64($f9_2, 30);
        $f9g6_19 = $g6_19->mulInt64($f9, 30);
        $f9g7_38 = $g7_19->mulInt64($f9_2, 30);
        $f9g8_19 = $g8_19->mulInt64($f9, 30);
        $f9g9_38 = $g9_19->mulInt64($f9_2, 30);

        // $h0 = $f0g0 + $f1g9_38 + $f2g8_19 + $f3g7_38 + $f4g6_19 + $f5g5_38 + $f6g4_19 + $f7g3_38 + $f8g2_19 + $f9g1_38;
        $h0 = $f0g0->addInt64($f1g9_38)->addInt64($f2g8_19)->addInt64($f3g7_38)
            ->addInt64($f4g6_19)->addInt64($f5g5_38)->addInt64($f6g4_19)
            ->addInt64($f7g3_38)->addInt64($f8g2_19)->addInt64($f9g1_38);

        // $h1 = $f0g1 + $f1g0    + $f2g9_19 + $f3g8_19 + $f4g7_19 + $f5g6_19 + $f6g5_19 + $f7g4_19 + $f8g3_19 + $f9g2_19;
        $h1 = $f0g1->addInt64($f1g0)->addInt64($f2g9_19)->addInt64($f3g8_19)
            ->addInt64($f4g7_19)->addInt64($f5g6_19)->addInt64($f6g5_19)
            ->addInt64($f7g4_19)->addInt64($f8g3_19)->addInt64($f9g2_19);

        // $h2 = $f0g2 + $f1g1_2  + $f2g0    + $f3g9_38 + $f4g8_19 + $f5g7_38 + $f6g6_19 + $f7g5_38 + $f8g4_19 + $f9g3_38;
        $h2 = $f0g2->addInt64($f1g1_2)->addInt64($f2g0)->addInt64($f3g9_38)
            ->addInt64($f4g8_19)->addInt64($f5g7_38)->addInt64($f6g6_19)
            ->addInt64($f7g5_38)->addInt64($f8g4_19)->addInt64($f9g3_38);

        // $h3 = $f0g3 + $f1g2    + $f2g1    + $f3g0    + $f4g9_19 + $f5g8_19 + $f6g7_19 + $f7g6_19 + $f8g5_19 + $f9g4_19;
        $h3 = $f0g3->addInt64($f1g2)->addInt64($f2g1)->addInt64($f3g0)
            ->addInt64($f4g9_19)->addInt64($f5g8_19)->addInt64($f6g7_19)
            ->addInt64($f7g6_19)->addInt64($f8g5_19)->addInt64($f9g4_19);

        // $h4 = $f0g4 + $f1g3_2  + $f2g2    + $f3g1_2  + $f4g0    + $f5g9_38 + $f6g8_19 + $f7g7_38 + $f8g6_19 + $f9g5_38;
        $h4 = $f0g4->addInt64($f1g3_2)->addInt64($f2g2)->addInt64($f3g1_2)
            ->addInt64($f4g0)->addInt64($f5g9_38)->addInt64($f6g8_19)
            ->addInt64($f7g7_38)->addInt64($f8g6_19)->addInt64($f9g5_38);

        // $h5 = $f0g5 + $f1g4    + $f2g3    + $f3g2    + $f4g1    + $f5g0    + $f6g9_19 + $f7g8_19 + $f8g7_19 + $f9g6_19;
        $h5 = $f0g5->addInt64($f1g4)->addInt64($f2g3)->addInt64($f3g2)
            ->addInt64($f4g1)->addInt64($f5g0)->addInt64($f6g9_19)
            ->addInt64($f7g8_19)->addInt64($f8g7_19)->addInt64($f9g6_19);

        // $h6 = $f0g6 + $f1g5_2  + $f2g4    + $f3g3_2  + $f4g2    + $f5g1_2  + $f6g0    + $f7g9_38 + $f8g8_19 + $f9g7_38;
        $h6 = $f0g6->addInt64($f1g5_2)->addInt64($f2g4)->addInt64($f3g3_2)
            ->addInt64($f4g2)->addInt64($f5g1_2)->addInt64($f6g0)
            ->addInt64($f7g9_38)->addInt64($f8g8_19)->addInt64($f9g7_38);

        // $h7 = $f0g7 + $f1g6    + $f2g5    + $f3g4    + $f4g3    + $f5g2    + $f6g1    + $f7g0    + $f8g9_19 + $f9g8_19;
        $h7 = $f0g7->addInt64($f1g6)->addInt64($f2g5)->addInt64($f3g4)
            ->addInt64($f4g3)->addInt64($f5g2)->addInt64($f6g1)
            ->addInt64($f7g0)->addInt64($f8g9_19)->addInt64($f9g8_19);

        // $h8 = $f0g8 + $f1g7_2  + $f2g6    + $f3g5_2  + $f4g4    + $f5g3_2  + $f6g2    + $f7g1_2  + $f8g0    + $f9g9_38;
        $h8 = $f0g8->addInt64($f1g7_2)->addInt64($f2g6)->addInt64($f3g5_2)
            ->addInt64($f4g4)->addInt64($f5g3_2)->addInt64($f6g2)
            ->addInt64($f7g1_2)->addInt64($f8g0)->addInt64($f9g9_38);

        // $h9 = $f0g9 + $f1g8    + $f2g7    + $f3g6    + $f4g5    + $f5g4    + $f6g3    + $f7g2    + $f8g1    + $f9g0   ;
        $h9 = $f0g9->addInt64($f1g8)->addInt64($f2g7)->addInt64($f3g6)
            ->addInt64($f4g5)->addInt64($f5g4)->addInt64($f6g3)
            ->addInt64($f7g2)->addInt64($f8g1)->addInt64($f9g0);

        /**
         * @var ParagonIE_Sodium_Core32_Int64 $h0
         * @var ParagonIE_Sodium_Core32_Int64 $h1
         * @var ParagonIE_Sodium_Core32_Int64 $h2
         * @var ParagonIE_Sodium_Core32_Int64 $h3
         * @var ParagonIE_Sodium_Core32_Int64 $h4
         * @var ParagonIE_Sodium_Core32_Int64 $h5
         * @var ParagonIE_Sodium_Core32_Int64 $h6
         * @var ParagonIE_Sodium_Core32_Int64 $h7
         * @var ParagonIE_Sodium_Core32_Int64 $h8
         * @var ParagonIE_Sodium_Core32_Int64 $h9
         * @var ParagonIE_Sodium_Core32_Int64 $carry0
         * @var ParagonIE_Sodium_Core32_Int64 $carry1
         * @var ParagonIE_Sodium_Core32_Int64 $carry2
         * @var ParagonIE_Sodium_Core32_Int64 $carry3
         * @var ParagonIE_Sodium_Core32_Int64 $carry4
         * @var ParagonIE_Sodium_Core32_Int64 $carry5
         * @var ParagonIE_Sodium_Core32_Int64 $carry6
         * @var ParagonIE_Sodium_Core32_Int64 $carry7
         * @var ParagonIE_Sodium_Core32_Int64 $carry8
         * @var ParagonIE_Sodium_Core32_Int64 $carry9
         */
        $carry0 = $h0->addInt(1 << 25)->shiftRight(26);
        $h1 = $h1->addInt64($carry0);
        $h0 = $h0->subInt64($carry0->shiftLeft(26));
        $carry4 = $h4->addInt(1 << 25)->shiftRight(26);
        $h5 = $h5->addInt64($carry4);
        $h4 = $h4->subInt64($carry4->shiftLeft(26));

        $carry1 = $h1->addInt(1 << 24)->shiftRight(25);
        $h2 = $h2->addInt64($carry1);
        $h1 = $h1->subInt64($carry1->shiftLeft(25));
        $carry5 = $h5->addInt(1 << 24)->shiftRight(25);
        $h6 = $h6->addInt64($carry5);
        $h5 = $h5->subInt64($carry5->shiftLeft(25));

        $carry2 = $h2->addInt(1 << 25)->shiftRight(26);
        $h3 = $h3->addInt64($carry2);
        $h2 = $h2->subInt64($carry2->shiftLeft(26));
        $carry6 = $h6->addInt(1 << 25)->shiftRight(26);
        $h7 = $h7->addInt64($carry6);
        $h6 = $h6->subInt64($carry6->shiftLeft(26));

        $carry3 = $h3->addInt(1 << 24)->shiftRight(25);
        $h4 = $h4->addInt64($carry3);
        $h3 = $h3->subInt64($carry3->shiftLeft(25));
        $carry7 = $h7->addInt(1 << 24)->shiftRight(25);
        $h8 = $h8->addInt64($carry7);
        $h7 = $h7->subInt64($carry7->shiftLeft(25));

        $carry4 = $h4->addInt(1 << 25)->shiftRight(26);
        $h5 = $h5->addInt64($carry4);
        $h4 = $h4->subInt64($carry4->shiftLeft(26));
        $carry8 = $h8->addInt(1 << 25)->shiftRight(26);
        $h9 = $h9->addInt64($carry8);
        $h8 = $h8->subInt64($carry8->shiftLeft(26));

        $carry9 = $h9->addInt(1 << 24)->shiftRight(25);
        $h0 = $h0->addInt64($carry9->mulInt(19, 5));
        $h9 = $h9->subInt64($carry9->shiftLeft(25));

        $carry0 = $h0->addInt(1 << 25)->shiftRight(26);
        $h1 = $h1->addInt64($carry0);
        $h0 = $h0->subInt64($carry0->shiftLeft(26));

        return ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
            array(
                $h0->toInt32(),
                $h1->toInt32(),
                $h2->toInt32(),
                $h3->toInt32(),
                $h4->toInt32(),
                $h5->toInt32(),
                $h6->toInt32(),
                $h7->toInt32(),
                $h8->toInt32(),
                $h9->toInt32()
            )
        );
    }

    /**
     * Get the negative values for each piece of the field element.
     *
     * h = -f
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $f
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedMethodCall
     */
    public static function fe_neg(ParagonIE_Sodium_Core32_Curve25519_Fe $f)
    {
        $h = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        for ($i = 0; $i < 10; ++$i) {
            $h[$i] = $h[$i]->subInt32($f[$i]);
        }
        return $h;
    }

    /**
     * Square a field element
     *
     * h = f * f
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $f
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedMethodCall
     */
    public static function fe_sq(ParagonIE_Sodium_Core32_Curve25519_Fe $f)
    {
        /** @var ParagonIE_Sodium_Core32_Int64 $f0 */
        $f0 = $f[0]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f1 */
        $f1 = $f[1]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f2 */
        $f2 = $f[2]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f3 */
        $f3 = $f[3]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f4 */
        $f4 = $f[4]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f5 */
        $f5 = $f[5]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f6 */
        $f6 = $f[6]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f7 */
        $f7 = $f[7]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f8 */
        $f8 = $f[8]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f9 */
        $f9 = $f[9]->toInt64();

        /** @var ParagonIE_Sodium_Core32_Int64 $f0_2 */
        $f0_2 = $f0->shiftLeft(1);
        $f1_2 = $f1->shiftLeft(1);
        $f2_2 = $f2->shiftLeft(1);
        $f3_2 = $f3->shiftLeft(1);
        $f4_2 = $f4->shiftLeft(1);
        $f5_2 = $f5->shiftLeft(1);
        $f6_2 = $f6->shiftLeft(1);
        $f7_2 = $f7->shiftLeft(1);
        $f5_38 = $f5->mulInt(38, 6);
        $f6_19 = $f6->mulInt(19, 5);
        $f7_38 = $f7->mulInt(38, 6);
        $f8_19 = $f8->mulInt(19, 5);
        $f9_38 = $f9->mulInt(38, 6);
        /** @var ParagonIE_Sodium_Core32_Int64 $f0f0*/
        $f0f0    = $f0->mulInt64($f0, 28);
        $f0f1_2  = $f0_2->mulInt64($f1, 28);
        $f0f2_2 =  $f0_2->mulInt64($f2, 28);
        $f0f3_2 =  $f0_2->mulInt64($f3, 28);
        $f0f4_2 =  $f0_2->mulInt64($f4, 28);
        $f0f5_2 =  $f0_2->mulInt64($f5, 28);
        $f0f6_2 =  $f0_2->mulInt64($f6, 28);
        $f0f7_2 =  $f0_2->mulInt64($f7, 28);
        $f0f8_2 =  $f0_2->mulInt64($f8, 28);
        $f0f9_2 =  $f0_2->mulInt64($f9, 28);

        $f1f1_2 = $f1_2->mulInt64($f1, 28);
        $f1f2_2 = $f1_2->mulInt64($f2, 28);
        $f1f3_4 = $f1_2->mulInt64($f3_2, 28);
        $f1f4_2 = $f1_2->mulInt64($f4, 28);
        $f1f5_4 = $f1_2->mulInt64($f5_2, 30);
        $f1f6_2 = $f1_2->mulInt64($f6, 28);
        $f1f7_4 = $f1_2->mulInt64($f7_2, 28);
        $f1f8_2 = $f1_2->mulInt64($f8, 28);
        $f1f9_76 = $f9_38->mulInt64($f1_2, 30);

        $f2f2 = $f2->mulInt64($f2, 28);
        $f2f3_2 = $f2_2->mulInt64($f3, 28);
        $f2f4_2 = $f2_2->mulInt64($f4, 28);
        $f2f5_2 = $f2_2->mulInt64($f5, 28);
        $f2f6_2 = $f2_2->mulInt64($f6, 28);
        $f2f7_2 = $f2_2->mulInt64($f7, 28);
        $f2f8_38 = $f8_19->mulInt64($f2_2, 30);
        $f2f9_38 = $f9_38->mulInt64($f2, 30);

        $f3f3_2 = $f3_2->mulInt64($f3, 28);
        $f3f4_2 = $f3_2->mulInt64($f4, 28);
        $f3f5_4 = $f3_2->mulInt64($f5_2, 30);
        $f3f6_2 = $f3_2->mulInt64($f6, 28);
        $f3f7_76 = $f7_38->mulInt64($f3_2, 30);
        $f3f8_38 = $f8_19->mulInt64($f3_2, 30);
        $f3f9_76 = $f9_38->mulInt64($f3_2, 30);

        $f4f4 = $f4->mulInt64($f4, 28);
        $f4f5_2 = $f4_2->mulInt64($f5, 28);
        $f4f6_38 = $f6_19->mulInt64($f4_2, 30);
        $f4f7_38 = $f7_38->mulInt64($f4, 30);
        $f4f8_38 = $f8_19->mulInt64($f4_2, 30);
        $f4f9_38 = $f9_38->mulInt64($f4, 30);

        $f5f5_38 = $f5_38->mulInt64($f5, 30);
        $f5f6_38 = $f6_19->mulInt64($f5_2, 30);
        $f5f7_76 = $f7_38->mulInt64($f5_2, 30);
        $f5f8_38 = $f8_19->mulInt64($f5_2, 30);
        $f5f9_76 = $f9_38->mulInt64($f5_2, 30);

        $f6f6_19 = $f6_19->mulInt64($f6, 30);
        $f6f7_38 = $f7_38->mulInt64($f6, 30);
        $f6f8_38 = $f8_19->mulInt64($f6_2, 30);
        $f6f9_38 = $f9_38->mulInt64($f6, 30);

        $f7f7_38 = $f7_38->mulInt64($f7, 28);
        $f7f8_38 = $f8_19->mulInt64($f7_2, 30);
        $f7f9_76 = $f9_38->mulInt64($f7_2, 30);

        $f8f8_19 = $f8_19->mulInt64($f8, 30);
        $f8f9_38 = $f9_38->mulInt64($f8, 30);

        $f9f9_38 = $f9_38->mulInt64($f9, 28);

        $h0 = $f0f0->addInt64($f1f9_76)->addInt64($f2f8_38)->addInt64($f3f7_76)->addInt64($f4f6_38)->addInt64($f5f5_38);
        $h1 = $f0f1_2->addInt64($f2f9_38)->addInt64($f3f8_38)->addInt64($f4f7_38)->addInt64($f5f6_38);
        $h2 = $f0f2_2->addInt64($f1f1_2)->addInt64($f3f9_76)->addInt64($f4f8_38)->addInt64($f5f7_76)->addInt64($f6f6_19);
        $h3 = $f0f3_2->addInt64($f1f2_2)->addInt64($f4f9_38)->addInt64($f5f8_38)->addInt64($f6f7_38);
        $h4 = $f0f4_2->addInt64($f1f3_4)->addInt64($f2f2)->addInt64($f5f9_76)->addInt64($f6f8_38)->addInt64($f7f7_38);
        $h5 = $f0f5_2->addInt64($f1f4_2)->addInt64($f2f3_2)->addInt64($f6f9_38)->addInt64($f7f8_38);
        $h6 = $f0f6_2->addInt64($f1f5_4)->addInt64($f2f4_2)->addInt64($f3f3_2)->addInt64($f7f9_76)->addInt64($f8f8_19);
        $h7 = $f0f7_2->addInt64($f1f6_2)->addInt64($f2f5_2)->addInt64($f3f4_2)->addInt64($f8f9_38);
        $h8 = $f0f8_2->addInt64($f1f7_4)->addInt64($f2f6_2)->addInt64($f3f5_4)->addInt64($f4f4)->addInt64($f9f9_38);
        $h9 = $f0f9_2->addInt64($f1f8_2)->addInt64($f2f7_2)->addInt64($f3f6_2)->addInt64($f4f5_2);

        /**
         * @var ParagonIE_Sodium_Core32_Int64 $h0
         * @var ParagonIE_Sodium_Core32_Int64 $h1
         * @var ParagonIE_Sodium_Core32_Int64 $h2
         * @var ParagonIE_Sodium_Core32_Int64 $h3
         * @var ParagonIE_Sodium_Core32_Int64 $h4
         * @var ParagonIE_Sodium_Core32_Int64 $h5
         * @var ParagonIE_Sodium_Core32_Int64 $h6
         * @var ParagonIE_Sodium_Core32_Int64 $h7
         * @var ParagonIE_Sodium_Core32_Int64 $h8
         * @var ParagonIE_Sodium_Core32_Int64 $h9
         */

        $carry0 = $h0->addInt(1 << 25)->shiftRight(26);
        $h1 = $h1->addInt64($carry0);
        $h0 = $h0->subInt64($carry0->shiftLeft(26));

        $carry4 = $h4->addInt(1 << 25)->shiftRight(26);
        $h5 = $h5->addInt64($carry4);
        $h4 = $h4->subInt64($carry4->shiftLeft(26));

        $carry1 = $h1->addInt(1 << 24)->shiftRight(25);
        $h2 = $h2->addInt64($carry1);
        $h1 = $h1->subInt64($carry1->shiftLeft(25));

        $carry5 = $h5->addInt(1 << 24)->shiftRight(25);
        $h6 = $h6->addInt64($carry5);
        $h5 = $h5->subInt64($carry5->shiftLeft(25));

        $carry2 = $h2->addInt(1 << 25)->shiftRight(26);
        $h3 = $h3->addInt64($carry2);
        $h2 = $h2->subInt64($carry2->shiftLeft(26));

        $carry6 = $h6->addInt(1 << 25)->shiftRight(26);
        $h7 = $h7->addInt64($carry6);
        $h6 = $h6->subInt64($carry6->shiftLeft(26));

        $carry3 = $h3->addInt(1 << 24)->shiftRight(25);
        $h4 = $h4->addInt64($carry3);
        $h3 = $h3->subInt64($carry3->shiftLeft(25));

        $carry7 = $h7->addInt(1 << 24)->shiftRight(25);
        $h8 = $h8->addInt64($carry7);
        $h7 = $h7->subInt64($carry7->shiftLeft(25));

        $carry4 = $h4->addInt(1 << 25)->shiftRight(26);
        $h5 = $h5->addInt64($carry4);
        $h4 = $h4->subInt64($carry4->shiftLeft(26));

        $carry8 = $h8->addInt(1 << 25)->shiftRight(26);
        $h9 = $h9->addInt64($carry8);
        $h8 = $h8->subInt64($carry8->shiftLeft(26));

        $carry9 = $h9->addInt(1 << 24)->shiftRight(25);
        $h0 = $h0->addInt64($carry9->mulInt(19, 5));
        $h9 = $h9->subInt64($carry9->shiftLeft(25));

        $carry0 = $h0->addInt(1 << 25)->shiftRight(26);
        $h1 = $h1->addInt64($carry0);
        $h0 = $h0->subInt64($carry0->shiftLeft(26));

        return ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
            array(
                $h0->toInt32(),
                $h1->toInt32(),
                $h2->toInt32(),
                $h3->toInt32(),
                $h4->toInt32(),
                $h5->toInt32(),
                $h6->toInt32(),
                $h7->toInt32(),
                $h8->toInt32(),
                $h9->toInt32()
            )
        );
    }

    /**
     * Square and double a field element
     *
     * h = 2 * f * f
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $f
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedMethodCall
     */
    public static function fe_sq2(ParagonIE_Sodium_Core32_Curve25519_Fe $f)
    {
        /** @var ParagonIE_Sodium_Core32_Int64 $f0 */
        $f0 = $f[0]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f1 */
        $f1 = $f[1]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f2 */
        $f2 = $f[2]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f3 */
        $f3 = $f[3]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f4 */
        $f4 = $f[4]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f5 */
        $f5 = $f[5]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f6 */
        $f6 = $f[6]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f7 */
        $f7 = $f[7]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f8 */
        $f8 = $f[8]->toInt64();
        /** @var ParagonIE_Sodium_Core32_Int64 $f9 */
        $f9 = $f[9]->toInt64();

        $f0_2 = $f0->shiftLeft(1);
        $f1_2 = $f1->shiftLeft(1);
        $f2_2 = $f2->shiftLeft(1);
        $f3_2 = $f3->shiftLeft(1);
        $f4_2 = $f4->shiftLeft(1);
        $f5_2 = $f5->shiftLeft(1);
        $f6_2 = $f6->shiftLeft(1);
        $f7_2 = $f7->shiftLeft(1);
        $f5_38 = $f5->mulInt(38, 6); /* 1.959375*2^30 */
        $f6_19 = $f6->mulInt(19, 5); /* 1.959375*2^30 */
        $f7_38 = $f7->mulInt(38, 6); /* 1.959375*2^30 */
        $f8_19 = $f8->mulInt(19, 5); /* 1.959375*2^30 */
        $f9_38 = $f9->mulInt(38, 6); /* 1.959375*2^30 */
        $f0f0 = $f0->mulInt64($f0, 28);
        $f0f1_2 = $f0_2->mulInt64($f1, 28);
        $f0f2_2 = $f0_2->mulInt64($f2, 28);
        $f0f3_2 = $f0_2->mulInt64($f3, 28);
        $f0f4_2 = $f0_2->mulInt64($f4, 28);
        $f0f5_2 = $f0_2->mulInt64($f5, 28);
        $f0f6_2 = $f0_2->mulInt64($f6, 28);
        $f0f7_2 = $f0_2->mulInt64($f7, 28);
        $f0f8_2 = $f0_2->mulInt64($f8, 28);
        $f0f9_2 = $f0_2->mulInt64($f9, 28);
        $f1f1_2 = $f1_2->mulInt64($f1, 28);
        $f1f2_2 = $f1_2->mulInt64($f2, 28);
        $f1f3_4 = $f1_2->mulInt64($f3_2, 29);
        $f1f4_2 = $f1_2->mulInt64($f4, 28);
        $f1f5_4 = $f1_2->mulInt64($f5_2, 29);
        $f1f6_2 = $f1_2->mulInt64($f6, 28);
        $f1f7_4 = $f1_2->mulInt64($f7_2, 29);
        $f1f8_2 = $f1_2->mulInt64($f8, 28);
        $f1f9_76 = $f9_38->mulInt64($f1_2, 29);
        $f2f2 = $f2->mulInt64($f2, 28);
        $f2f3_2 = $f2_2->mulInt64($f3, 28);
        $f2f4_2 = $f2_2->mulInt64($f4, 28);
        $f2f5_2 = $f2_2->mulInt64($f5, 28);
        $f2f6_2 = $f2_2->mulInt64($f6, 28);
        $f2f7_2 = $f2_2->mulInt64($f7, 28);
        $f2f8_38 = $f8_19->mulInt64($f2_2, 29);
        $f2f9_38 = $f9_38->mulInt64($f2, 29);
        $f3f3_2 = $f3_2->mulInt64($f3, 28);
        $f3f4_2 = $f3_2->mulInt64($f4, 28);
        $f3f5_4 = $f3_2->mulInt64($f5_2, 28);
        $f3f6_2 = $f3_2->mulInt64($f6, 28);
        $f3f7_76 = $f7_38->mulInt64($f3_2, 29);
        $f3f8_38 = $f8_19->mulInt64($f3_2, 29);
        $f3f9_76 = $f9_38->mulInt64($f3_2, 29);
        $f4f4 = $f4->mulInt64($f4, 28);
        $f4f5_2 = $f4_2->mulInt64($f5, 28);
        $f4f6_38 = $f6_19->mulInt64($f4_2, 29);
        $f4f7_38 = $f7_38->mulInt64($f4, 29);
        $f4f8_38 = $f8_19->mulInt64($f4_2, 29);
        $f4f9_38 = $f9_38->mulInt64($f4, 29);
        $f5f5_38 = $f5_38->mulInt64($f5, 29);
        $f5f6_38 = $f6_19->mulInt64($f5_2, 29);
        $f5f7_76 = $f7_38->mulInt64($f5_2, 29);
        $f5f8_38 = $f8_19->mulInt64($f5_2, 29);
        $f5f9_76 = $f9_38->mulInt64($f5_2, 29);
        $f6f6_19 = $f6_19->mulInt64($f6, 29);
        $f6f7_38 = $f7_38->mulInt64($f6, 29);
        $f6f8_38 = $f8_19->mulInt64($f6_2, 29);
        $f6f9_38 = $f9_38->mulInt64($f6, 29);
        $f7f7_38 = $f7_38->mulInt64($f7, 29);
        $f7f8_38 = $f8_19->mulInt64($f7_2, 29);
        $f7f9_76 = $f9_38->mulInt64($f7_2, 29);
        $f8f8_19 = $f8_19->mulInt64($f8, 29);
        $f8f9_38 = $f9_38->mulInt64($f8, 29);
        $f9f9_38 = $f9_38->mulInt64($f9, 29);

        $h0 = $f0f0->addInt64($f1f9_76)->addInt64($f2f8_38)->addInt64($f3f7_76)->addInt64($f4f6_38)->addInt64($f5f5_38);
        $h1 = $f0f1_2->addInt64($f2f9_38)->addInt64($f3f8_38)->addInt64($f4f7_38)->addInt64($f5f6_38);
        $h2 = $f0f2_2->addInt64($f1f1_2)->addInt64($f3f9_76)->addInt64($f4f8_38)->addInt64($f5f7_76)->addInt64($f6f6_19);
        $h3 = $f0f3_2->addInt64($f1f2_2)->addInt64($f4f9_38)->addInt64($f5f8_38)->addInt64($f6f7_38);
        $h4 = $f0f4_2->addInt64($f1f3_4)->addInt64($f2f2)->addInt64($f5f9_76)->addInt64($f6f8_38)->addInt64($f7f7_38);
        $h5 = $f0f5_2->addInt64($f1f4_2)->addInt64($f2f3_2)->addInt64($f6f9_38)->addInt64($f7f8_38);
        $h6 = $f0f6_2->addInt64($f1f5_4)->addInt64($f2f4_2)->addInt64($f3f3_2)->addInt64($f7f9_76)->addInt64($f8f8_19);
        $h7 = $f0f7_2->addInt64($f1f6_2)->addInt64($f2f5_2)->addInt64($f3f4_2)->addInt64($f8f9_38);
        $h8 = $f0f8_2->addInt64($f1f7_4)->addInt64($f2f6_2)->addInt64($f3f5_4)->addInt64($f4f4)->addInt64($f9f9_38);
        $h9 = $f0f9_2->addInt64($f1f8_2)->addInt64($f2f7_2)->addInt64($f3f6_2)->addInt64($f4f5_2);

        /**
         * @var ParagonIE_Sodium_Core32_Int64 $h0
         * @var ParagonIE_Sodium_Core32_Int64 $h1
         * @var ParagonIE_Sodium_Core32_Int64 $h2
         * @var ParagonIE_Sodium_Core32_Int64 $h3
         * @var ParagonIE_Sodium_Core32_Int64 $h4
         * @var ParagonIE_Sodium_Core32_Int64 $h5
         * @var ParagonIE_Sodium_Core32_Int64 $h6
         * @var ParagonIE_Sodium_Core32_Int64 $h7
         * @var ParagonIE_Sodium_Core32_Int64 $h8
         * @var ParagonIE_Sodium_Core32_Int64 $h9
         */
        $h0 = $h0->shiftLeft(1);
        $h1 = $h1->shiftLeft(1);
        $h2 = $h2->shiftLeft(1);
        $h3 = $h3->shiftLeft(1);
        $h4 = $h4->shiftLeft(1);
        $h5 = $h5->shiftLeft(1);
        $h6 = $h6->shiftLeft(1);
        $h7 = $h7->shiftLeft(1);
        $h8 = $h8->shiftLeft(1);
        $h9 = $h9->shiftLeft(1);

        $carry0 = $h0->addInt(1 << 25)->shiftRight(26);
        $h1 = $h1->addInt64($carry0);
        $h0 = $h0->subInt64($carry0->shiftLeft(26));
        $carry4 = $h4->addInt(1 << 25)->shiftRight(26);
        $h5 = $h5->addInt64($carry4);
        $h4 = $h4->subInt64($carry4->shiftLeft(26));

        $carry1 = $h1->addInt(1 << 24)->shiftRight(25);
        $h2 = $h2->addInt64($carry1);
        $h1 = $h1->subInt64($carry1->shiftLeft(25));
        $carry5 = $h5->addInt(1 << 24)->shiftRight(25);
        $h6 = $h6->addInt64($carry5);
        $h5 = $h5->subInt64($carry5->shiftLeft(25));

        $carry2 = $h2->addInt(1 << 25)->shiftRight(26);
        $h3 = $h3->addInt64($carry2);
        $h2 = $h2->subInt64($carry2->shiftLeft(26));
        $carry6 = $h6->addInt(1 << 25)->shiftRight(26);
        $h7 = $h7->addInt64($carry6);
        $h6 = $h6->subInt64($carry6->shiftLeft(26));

        $carry3 = $h3->addInt(1 << 24)->shiftRight(25);
        $h4 = $h4->addInt64($carry3);
        $h3 = $h3->subInt64($carry3->shiftLeft(25));
        $carry7 = $h7->addInt(1 << 24)->shiftRight(25);
        $h8 = $h8->addInt64($carry7);
        $h7 = $h7->subInt64($carry7->shiftLeft(25));

        $carry4 = $h4->addInt(1 << 25)->shiftRight(26);
        $h5 = $h5->addInt64($carry4);
        $h4 = $h4->subInt64($carry4->shiftLeft(26));
        $carry8 = $h8->addInt(1 << 25)->shiftRight(26);
        $h9 = $h9->addInt64($carry8);
        $h8 = $h8->subInt64($carry8->shiftLeft(26));

        $carry9 = $h9->addInt(1 << 24)->shiftRight(25);
        $h0 = $h0->addInt64($carry9->mulInt(19, 5));
        $h9 = $h9->subInt64($carry9->shiftLeft(25));

        $carry0 = $h0->addInt(1 << 25)->shiftRight(26);
        $h1 = $h1->addInt64($carry0);
        $h0 = $h0->subInt64($carry0->shiftLeft(26));

        return ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
            array(
                $h0->toInt32(),
                $h1->toInt32(),
                $h2->toInt32(),
                $h3->toInt32(),
                $h4->toInt32(),
                $h5->toInt32(),
                $h6->toInt32(),
                $h7->toInt32(),
                $h8->toInt32(),
                $h9->toInt32()
            )
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $Z
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fe_invert(ParagonIE_Sodium_Core32_Curve25519_Fe $Z)
    {
        $z = clone $Z;
        $t0 = self::fe_sq($z);
        $t1 = self::fe_sq($t0);
        $t1 = self::fe_sq($t1);
        $t1 = self::fe_mul($z, $t1);
        $t0 = self::fe_mul($t0, $t1);
        $t2 = self::fe_sq($t0);
        $t1 = self::fe_mul($t1, $t2);
        $t2 = self::fe_sq($t1);
        for ($i = 1; $i < 5; ++$i) {
            $t2 = self::fe_sq($t2);
        }
        $t1 = self::fe_mul($t2, $t1);
        $t2 = self::fe_sq($t1);
        for ($i = 1; $i < 10; ++$i) {
            $t2 = self::fe_sq($t2);
        }
        $t2 = self::fe_mul($t2, $t1);
        $t3 = self::fe_sq($t2);
        for ($i = 1; $i < 20; ++$i) {
            $t3 = self::fe_sq($t3);
        }
        $t2 = self::fe_mul($t3, $t2);
        $t2 = self::fe_sq($t2);
        for ($i = 1; $i < 10; ++$i) {
            $t2 = self::fe_sq($t2);
        }
        $t1 = self::fe_mul($t2, $t1);
        $t2 = self::fe_sq($t1);
        for ($i = 1; $i < 50; ++$i) {
            $t2 = self::fe_sq($t2);
        }
        $t2 = self::fe_mul($t2, $t1);
        $t3 = self::fe_sq($t2);
        for ($i = 1; $i < 100; ++$i) {
            $t3 = self::fe_sq($t3);
        }
        $t2 = self::fe_mul($t3, $t2);
        $t2 = self::fe_sq($t2);
        for ($i = 1; $i < 50; ++$i) {
            $t2 = self::fe_sq($t2);
        }
        $t1 = self::fe_mul($t2, $t1);
        $t1 = self::fe_sq($t1);
        for ($i = 1; $i < 5; ++$i) {
            $t1 = self::fe_sq($t1);
        }
        return self::fe_mul($t1, $t0);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @ref https://github.com/jedisct1/libsodium/blob/68564326e1e9dc57ef03746f85734232d20ca6fb/src/libsodium/crypto_core/curve25519/ref10/curve25519_ref10.c#L1054-L1106
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $z
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fe_pow22523(ParagonIE_Sodium_Core32_Curve25519_Fe $z)
    {
        # fe_sq(t0, z);
        # fe_sq(t1, t0);
        # fe_sq(t1, t1);
        # fe_mul(t1, z, t1);
        # fe_mul(t0, t0, t1);
        # fe_sq(t0, t0);
        # fe_mul(t0, t1, t0);
        # fe_sq(t1, t0);
        $t0 = self::fe_sq($z);
        $t1 = self::fe_sq($t0);
        $t1 = self::fe_sq($t1);
        $t1 = self::fe_mul($z, $t1);
        $t0 = self::fe_mul($t0, $t1);
        $t0 = self::fe_sq($t0);
        $t0 = self::fe_mul($t1, $t0);
        $t1 = self::fe_sq($t0);

        # for (i = 1; i < 5; ++i) {
        #     fe_sq(t1, t1);
        # }
        for ($i = 1; $i < 5; ++$i) {
            $t1 = self::fe_sq($t1);
        }

        # fe_mul(t0, t1, t0);
        # fe_sq(t1, t0);
        $t0 = self::fe_mul($t1, $t0);
        $t1 = self::fe_sq($t0);

        # for (i = 1; i < 10; ++i) {
        #     fe_sq(t1, t1);
        # }
        for ($i = 1; $i < 10; ++$i) {
            $t1 = self::fe_sq($t1);
        }

        # fe_mul(t1, t1, t0);
        # fe_sq(t2, t1);
        $t1 = self::fe_mul($t1, $t0);
        $t2 = self::fe_sq($t1);

        # for (i = 1; i < 20; ++i) {
        #     fe_sq(t2, t2);
        # }
        for ($i = 1; $i < 20; ++$i) {
            $t2 = self::fe_sq($t2);
        }

        # fe_mul(t1, t2, t1);
        # fe_sq(t1, t1);
        $t1 = self::fe_mul($t2, $t1);
        $t1 = self::fe_sq($t1);

        # for (i = 1; i < 10; ++i) {
        #     fe_sq(t1, t1);
        # }
        for ($i = 1; $i < 10; ++$i) {
            $t1 = self::fe_sq($t1);
        }

        # fe_mul(t0, t1, t0);
        # fe_sq(t1, t0);
        $t0 = self::fe_mul($t1, $t0);
        $t1 = self::fe_sq($t0);

        # for (i = 1; i < 50; ++i) {
        #     fe_sq(t1, t1);
        # }
        for ($i = 1; $i < 50; ++$i) {
            $t1 = self::fe_sq($t1);
        }

        # fe_mul(t1, t1, t0);
        # fe_sq(t2, t1);
        $t1 = self::fe_mul($t1, $t0);
        $t2 = self::fe_sq($t1);

        # for (i = 1; i < 100; ++i) {
        #     fe_sq(t2, t2);
        # }
        for ($i = 1; $i < 100; ++$i) {
            $t2 = self::fe_sq($t2);
        }

        # fe_mul(t1, t2, t1);
        # fe_sq(t1, t1);
        $t1 = self::fe_mul($t2, $t1);
        $t1 = self::fe_sq($t1);

        # for (i = 1; i < 50; ++i) {
        #     fe_sq(t1, t1);
        # }
        for ($i = 1; $i < 50; ++$i) {
            $t1 = self::fe_sq($t1);
        }

        # fe_mul(t0, t1, t0);
        # fe_sq(t0, t0);
        # fe_sq(t0, t0);
        # fe_mul(out, t0, z);
        $t0 = self::fe_mul($t1, $t0);
        $t0 = self::fe_sq($t0);
        $t0 = self::fe_sq($t0);
        return self::fe_mul($t0, $z);
    }

    /**
     * Subtract two field elements.
     *
     * h = f - g
     *
     * Preconditions:
     * |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc.
     * |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc.
     *
     * Postconditions:
     * |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $f
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $g
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedMethodCall
     * @psalm-suppress MixedTypeCoercion
     */
    public static function fe_sub(ParagonIE_Sodium_Core32_Curve25519_Fe $f, ParagonIE_Sodium_Core32_Curve25519_Fe $g)
    {
        return ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
            array(
                $f[0]->subInt32($g[0]),
                $f[1]->subInt32($g[1]),
                $f[2]->subInt32($g[2]),
                $f[3]->subInt32($g[3]),
                $f[4]->subInt32($g[4]),
                $f[5]->subInt32($g[5]),
                $f[6]->subInt32($g[6]),
                $f[7]->subInt32($g[7]),
                $f[8]->subInt32($g[8]),
                $f[9]->subInt32($g[9])
            )
        );
    }

    /**
     * Add two group elements.
     *
     * r = p + q
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_Cached $q
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_add(
        ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p,
        ParagonIE_Sodium_Core32_Curve25519_Ge_Cached $q
    ) {
        $r = new ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1();
        $r->X = self::fe_add($p->Y, $p->X);
        $r->Y = self::fe_sub($p->Y, $p->X);
        $r->Z = self::fe_mul($r->X, $q->YplusX);
        $r->Y = self::fe_mul($r->Y, $q->YminusX);
        $r->T = self::fe_mul($q->T2d, $p->T);
        $r->X = self::fe_mul($p->Z, $q->Z);
        $t0   = self::fe_add($r->X, $r->X);
        $r->X = self::fe_sub($r->Z, $r->Y);
        $r->Y = self::fe_add($r->Z, $r->Y);
        $r->Z = self::fe_add($t0, $r->T);
        $r->T = self::fe_sub($t0, $r->T);
        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @ref https://github.com/jedisct1/libsodium/blob/157c4a80c13b117608aeae12178b2d38825f9f8f/src/libsodium/crypto_core/curve25519/ref10/curve25519_ref10.c#L1185-L1215
     * @param string $a
     * @return array<int, mixed>
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArrayOffset
     */
    public static function slide($a)
    {
        if (self::strlen($a) < 256) {
            if (self::strlen($a) < 16) {
                $a = str_pad($a, 256, '0', STR_PAD_RIGHT);
            }
        }
        /** @var array<int, int> $r */
        $r = array();
        for ($i = 0; $i < 256; ++$i) {
            $r[$i] = (int) (1 &
                (
                    self::chrToInt($a[$i >> 3])
                        >>
                    ($i & 7)
                )
            );
        }

        for ($i = 0;$i < 256;++$i) {
            if ($r[$i]) {
                for ($b = 1;$b <= 6 && $i + $b < 256;++$b) {
                    if ($r[$i + $b]) {
                        if ($r[$i] + ($r[$i + $b] << $b) <= 15) {
                            $r[$i] += $r[$i + $b] << $b;
                            $r[$i + $b] = 0;
                        } elseif ($r[$i] - ($r[$i + $b] << $b) >= -15) {
                            $r[$i] -= $r[$i + $b] << $b;
                            for ($k = $i + $b; $k < 256; ++$k) {
                                if (!$r[$k]) {
                                    $r[$k] = 1;
                                    break;
                                }
                                $r[$k] = 0;
                            }
                        } else {
                            break;
                        }
                    }
                }
            }
        }
        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $s
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P3
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_frombytes_negate_vartime($s)
    {
        static $d = null;
        if (!$d) {
            /** @var ParagonIE_Sodium_Core32_Curve25519_Fe $d */
            $d = ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
                array(
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d[0]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d[1]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d[2]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d[3]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d[4]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d[5]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d[6]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d[7]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d[8]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d[9])
                )
            );
        }

        # fe_frombytes(h->Y,s);
        # fe_1(h->Z);
        $h = new ParagonIE_Sodium_Core32_Curve25519_Ge_P3(
            self::fe_0(),
            self::fe_frombytes($s),
            self::fe_1()
        );

        # fe_sq(u,h->Y);
        # fe_mul(v,u,d);
        # fe_sub(u,u,h->Z);       /* u = y^2-1 */
        # fe_add(v,v,h->Z);       /* v = dy^2+1 */
        $u = self::fe_sq($h->Y);
        /** @var ParagonIE_Sodium_Core32_Curve25519_Fe $d */
        $v = self::fe_mul($u, $d);
        $u = self::fe_sub($u, $h->Z); /* u =  y^2 - 1 */
        $v = self::fe_add($v, $h->Z); /* v = dy^2 + 1 */

        # fe_sq(v3,v);
        # fe_mul(v3,v3,v);        /* v3 = v^3 */
        # fe_sq(h->X,v3);
        # fe_mul(h->X,h->X,v);
        # fe_mul(h->X,h->X,u);    /* x = uv^7 */
        $v3 = self::fe_sq($v);
        $v3 = self::fe_mul($v3, $v); /* v3 = v^3 */
        $h->X = self::fe_sq($v3);
        $h->X = self::fe_mul($h->X, $v);
        $h->X = self::fe_mul($h->X, $u); /* x = uv^7 */

        # fe_pow22523(h->X,h->X); /* x = (uv^7)^((q-5)/8) */
        # fe_mul(h->X,h->X,v3);
        # fe_mul(h->X,h->X,u);    /* x = uv^3(uv^7)^((q-5)/8) */
        $h->X = self::fe_pow22523($h->X); /* x = (uv^7)^((q-5)/8) */
        $h->X = self::fe_mul($h->X, $v3);
        $h->X = self::fe_mul($h->X, $u); /* x = uv^3(uv^7)^((q-5)/8) */

        # fe_sq(vxx,h->X);
        # fe_mul(vxx,vxx,v);
        # fe_sub(check,vxx,u);    /* vx^2-u */
        $vxx = self::fe_sq($h->X);
        $vxx = self::fe_mul($vxx, $v);
        $check = self::fe_sub($vxx, $u); /* vx^2 - u */

        # if (fe_isnonzero(check)) {
        #     fe_add(check,vxx,u);  /* vx^2+u */
        #     if (fe_isnonzero(check)) {
        #         return -1;
        #     }
        #     fe_mul(h->X,h->X,sqrtm1);
        # }
        if (self::fe_isnonzero($check)) {
            $check = self::fe_add($vxx, $u); /* vx^2 + u */
            if (self::fe_isnonzero($check)) {
                throw new RangeException('Internal check failed.');
            }
            $h->X = self::fe_mul(
                $h->X,
                ParagonIE_Sodium_Core32_Curve25519_Fe::fromIntArray(self::$sqrtm1)
            );
        }

        # if (fe_isnegative(h->X) == (s[31] >> 7)) {
        #     fe_neg(h->X,h->X);
        # }
        $i = self::chrToInt($s[31]);
        if (self::fe_isnegative($h->X) === ($i >> 7)) {
            $h->X = self::fe_neg($h->X);
        }

        # fe_mul(h->T,h->X,h->Y);
        $h->T = self::fe_mul($h->X, $h->Y);
        return $h;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1 $R
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp $q
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_madd(
        ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1 $R,
        ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p,
        ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp $q
    ) {
        $r = clone $R;
        $r->X = self::fe_add($p->Y, $p->X);
        $r->Y = self::fe_sub($p->Y, $p->X);
        $r->Z = self::fe_mul($r->X, $q->yplusx);
        $r->Y = self::fe_mul($r->Y, $q->yminusx);
        $r->T = self::fe_mul($q->xy2d, $p->T);
        $t0 = self::fe_add(clone $p->Z, clone $p->Z);
        $r->X = self::fe_sub($r->Z, $r->Y);
        $r->Y = self::fe_add($r->Z, $r->Y);
        $r->Z = self::fe_add($t0, $r->T);
        $r->T = self::fe_sub($t0, $r->T);

        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1 $R
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp $q
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_msub(
        ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1 $R,
        ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p,
        ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp $q
    ) {
        $r = clone $R;

        $r->X = self::fe_add($p->Y, $p->X);
        $r->Y = self::fe_sub($p->Y, $p->X);
        $r->Z = self::fe_mul($r->X, $q->yminusx);
        $r->Y = self::fe_mul($r->Y, $q->yplusx);
        $r->T = self::fe_mul($q->xy2d, $p->T);
        $t0 = self::fe_add($p->Z, $p->Z);
        $r->X = self::fe_sub($r->Z, $r->Y);
        $r->Y = self::fe_add($r->Z, $r->Y);
        $r->Z = self::fe_sub($t0, $r->T);
        $r->T = self::fe_add($t0, $r->T);

        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1 $p
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P2
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_p1p1_to_p2(ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1 $p)
    {
        $r = new ParagonIE_Sodium_Core32_Curve25519_Ge_P2();
        $r->X = self::fe_mul($p->X, $p->T);
        $r->Y = self::fe_mul($p->Y, $p->Z);
        $r->Z = self::fe_mul($p->Z, $p->T);
        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1 $p
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P3
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_p1p1_to_p3(ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1 $p)
    {
        $r = new ParagonIE_Sodium_Core32_Curve25519_Ge_P3();
        $r->X = self::fe_mul($p->X, $p->T);
        $r->Y = self::fe_mul($p->Y, $p->Z);
        $r->Z = self::fe_mul($p->Z, $p->T);
        $r->T = self::fe_mul($p->X, $p->Y);
        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P2
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_p2_0()
    {
        return new ParagonIE_Sodium_Core32_Curve25519_Ge_P2(
            self::fe_0(),
            self::fe_1(),
            self::fe_1()
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P2 $p
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_p2_dbl(ParagonIE_Sodium_Core32_Curve25519_Ge_P2 $p)
    {
        $r = new ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1();

        $r->X = self::fe_sq($p->X);
        $r->Z = self::fe_sq($p->Y);
        $r->T = self::fe_sq2($p->Z);
        $r->Y = self::fe_add($p->X, $p->Y);
        $t0   = self::fe_sq($r->Y);
        $r->Y = self::fe_add($r->Z, $r->X);
        $r->Z = self::fe_sub($r->Z, $r->X);
        $r->X = self::fe_sub($t0, $r->Y);
        $r->T = self::fe_sub($r->T, $r->Z);

        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P3
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_p3_0()
    {
        return new ParagonIE_Sodium_Core32_Curve25519_Ge_P3(
            self::fe_0(),
            self::fe_1(),
            self::fe_1(),
            self::fe_0()
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_Cached
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_p3_to_cached(ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p)
    {
        static $d2 = null;
        if ($d2 === null) {
            $d2 = ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
                array(
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d2[0]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d2[1]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d2[2]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d2[3]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d2[4]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d2[5]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d2[6]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d2[7]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d2[8]),
                    ParagonIE_Sodium_Core32_Int32::fromInt(self::$d2[9])
                )
            );
        }
        /** @var ParagonIE_Sodium_Core32_Curve25519_Fe $d2 */
        $r = new ParagonIE_Sodium_Core32_Curve25519_Ge_Cached();
        $r->YplusX = self::fe_add($p->Y, $p->X);
        $r->YminusX = self::fe_sub($p->Y, $p->X);
        $r->Z = self::fe_copy($p->Z);
        $r->T2d = self::fe_mul($p->T, $d2);
        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P2
     */
    public static function ge_p3_to_p2(ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p)
    {
        return new ParagonIE_Sodium_Core32_Curve25519_Ge_P2(
            $p->X,
            $p->Y,
            $p->Z
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $h
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_p3_tobytes(ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $h)
    {
        $recip = self::fe_invert($h->Z);
        $x = self::fe_mul($h->X, $recip);
        $y = self::fe_mul($h->Y, $recip);
        $s = self::fe_tobytes($y);
        $s[31] = self::intToChr(
            self::chrToInt($s[31]) ^ (self::fe_isnegative($x) << 7)
        );
        return $s;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_p3_dbl(ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p)
    {
        $q = self::ge_p3_to_p2($p);
        return self::ge_p2_dbl($q);
    }

    /**
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_precomp_0()
    {
        return new ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp(
            self::fe_1(),
            self::fe_1(),
            self::fe_0()
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $b
     * @param int $c
     * @return int
     * @psalm-suppress MixedReturnStatement
     */
    public static function equal($b, $c)
    {
        return (int) ((($b ^ $c) - 1 & 0xffffffff) >> 31);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string|int $char
     * @return int (1 = yes, 0 = no)
     * @throws SodiumException
     * @throws TypeError
     */
    public static function negative($char)
    {
        if (is_int($char)) {
            return $char < 0 ? 1 : 0;
        }
        /** @var string $char */
        /** @var int $x */
        $x = self::chrToInt(self::substr($char, 0, 1));
        return (int) ($x >> 31);
    }

    /**
     * Conditional move
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp $t
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp $u
     * @param int $b
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp
     * @throws SodiumException
     * @throws TypeError
     */
    public static function cmov(
        ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp $t,
        ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp $u,
        $b
    ) {
        if (!is_int($b)) {
            throw new InvalidArgumentException('Expected an integer.');
        }
        return new ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp(
            self::fe_cmov($t->yplusx, $u->yplusx, $b),
            self::fe_cmov($t->yminusx, $u->yminusx, $b),
            self::fe_cmov($t->xy2d, $u->xy2d, $b)
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $pos
     * @param int $b
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayOffset
     * @psalm-suppress MixedArgument
     */
    public static function ge_select($pos = 0, $b = 0)
    {
        static $base = null;
        if ($base === null) {
            $base = array();
            foreach (self::$base as $i => $bas) {
                for ($j = 0; $j < 8; ++$j) {
                    $base[$i][$j] = new ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp(
                        ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
                            array(
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][0][0]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][0][1]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][0][2]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][0][3]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][0][4]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][0][5]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][0][6]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][0][7]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][0][8]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][0][9])
                            )
                        ),
                        ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
                            array(
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][1][0]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][1][1]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][1][2]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][1][3]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][1][4]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][1][5]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][1][6]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][1][7]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][1][8]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][1][9])
                            )
                        ),
                        ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
                            array(
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][2][0]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][2][1]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][2][2]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][2][3]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][2][4]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][2][5]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][2][6]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][2][7]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][2][8]),
                                ParagonIE_Sodium_Core32_Int32::fromInt($bas[$j][2][9])
                            )
                        )
                    );
                }
            }
        }
        if (!is_int($pos)) {
            throw new InvalidArgumentException('Position must be an integer');
        }
        if ($pos < 0 || $pos > 31) {
            throw new RangeException('Position is out of range [0, 31]');
        }

        $bnegative = self::negative($b);
        /** @var int $babs */
        $babs = $b - (((-$bnegative) & $b) << 1);

        $t = self::ge_precomp_0();
        for ($i = 0; $i < 8; ++$i) {
            $t = self::cmov(
                $t,
                $base[$pos][$i],
                self::equal($babs, $i + 1)
            );
        }
        $minusT = new ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp(
            self::fe_copy($t->yminusx),
            self::fe_copy($t->yplusx),
            self::fe_neg($t->xy2d)
        );
        return self::cmov($t, $minusT, -$bnegative);
    }

    /**
     * Subtract two group elements.
     *
     * r = p - q
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_Cached $q
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_sub(
        ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $p,
        ParagonIE_Sodium_Core32_Curve25519_Ge_Cached $q
    ) {
        $r = new ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1();

        $r->X = self::fe_add($p->Y, $p->X);
        $r->Y = self::fe_sub($p->Y, $p->X);
        $r->Z = self::fe_mul($r->X, $q->YminusX);
        $r->Y = self::fe_mul($r->Y, $q->YplusX);
        $r->T = self::fe_mul($q->T2d, $p->T);
        $r->X = self::fe_mul($p->Z, $q->Z);
        $t0 = self::fe_add($r->X, $r->X);
        $r->X = self::fe_sub($r->Z, $r->Y);
        $r->Y = self::fe_add($r->Z, $r->Y);
        $r->Z = self::fe_sub($t0, $r->T);
        $r->T = self::fe_add($t0, $r->T);

        return $r;
    }

    /**
     * Convert a group element to a byte string.
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P2 $h
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_tobytes(ParagonIE_Sodium_Core32_Curve25519_Ge_P2 $h)
    {
        $recip = self::fe_invert($h->Z);
        $x = self::fe_mul($h->X, $recip);
        $y = self::fe_mul($h->Y, $recip);
        $s = self::fe_tobytes($y);
        $s[31] = self::intToChr(
            self::chrToInt($s[31]) ^ (self::fe_isnegative($x) << 7)
        );
        return $s;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $a
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $A
     * @param string $b
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P2
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArrayAccess
     */
    public static function ge_double_scalarmult_vartime(
        $a,
        ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $A,
        $b
    ) {
        /** @var array<int, ParagonIE_Sodium_Core32_Curve25519_Ge_Cached> $Ai */
        $Ai = array();

        static $Bi = array();
        /** @var array<int, ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp> $Bi */
        if (!$Bi) {
            for ($i = 0; $i < 8; ++$i) {
                $Bi[$i] = new ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp(
                    ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
                        array(
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][0][0]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][0][1]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][0][2]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][0][3]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][0][4]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][0][5]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][0][6]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][0][7]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][0][8]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][0][9])
                        )
                    ),
                    ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
                        array(
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][1][0]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][1][1]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][1][2]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][1][3]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][1][4]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][1][5]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][1][6]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][1][7]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][1][8]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][1][9])
                        )
                    ),
                    ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray(
                        array(
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][2][0]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][2][1]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][2][2]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][2][3]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][2][4]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][2][5]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][2][6]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][2][7]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][2][8]),
                            ParagonIE_Sodium_Core32_Int32::fromInt(self::$base2[$i][2][9])
                        )
                    )
                );
            }
        }

        for ($i = 0; $i < 8; ++$i) {
            $Ai[$i] = new ParagonIE_Sodium_Core32_Curve25519_Ge_Cached(
                self::fe_0(),
                self::fe_0(),
                self::fe_0(),
                self::fe_0()
            );
        }
        /** @var array<int, ParagonIE_Sodium_Core32_Curve25519_Ge_Cached> $Ai */

        # slide(aslide,a);
        # slide(bslide,b);
        /** @var array<int, int> $aslide */
        $aslide = self::slide($a);
        /** @var array<int, int> $bslide */
        $bslide = self::slide($b);

        # ge_p3_to_cached(&Ai[0],A);
        # ge_p3_dbl(&t,A); ge_p1p1_to_p3(&A2,&t);
        $Ai[0] = self::ge_p3_to_cached($A);
        $t = self::ge_p3_dbl($A);
        $A2 = self::ge_p1p1_to_p3($t);

        # ge_add(&t,&A2,&Ai[0]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[1],&u);
        # ge_add(&t,&A2,&Ai[1]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[2],&u);
        # ge_add(&t,&A2,&Ai[2]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[3],&u);
        # ge_add(&t,&A2,&Ai[3]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[4],&u);
        # ge_add(&t,&A2,&Ai[4]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[5],&u);
        # ge_add(&t,&A2,&Ai[5]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[6],&u);
        # ge_add(&t,&A2,&Ai[6]); ge_p1p1_to_p3(&u,&t); ge_p3_to_cached(&Ai[7],&u);
        for ($i = 0; $i < 7; ++$i) {
            $t = self::ge_add($A2, $Ai[$i]);
            $u = self::ge_p1p1_to_p3($t);
            $Ai[$i + 1] = self::ge_p3_to_cached($u);
        }

        # ge_p2_0(r);
        $r = self::ge_p2_0();

        # for (i = 255;i >= 0;--i) {
        #     if (aslide[i] || bslide[i]) break;
        # }
        $i = 255;
        for (; $i >= 0; --$i) {
            if ($aslide[$i] || $bslide[$i]) {
                break;
            }
        }

        # for (;i >= 0;--i) {
        for (; $i >= 0; --$i) {
            # ge_p2_dbl(&t,r);
            $t = self::ge_p2_dbl($r);

            # if (aslide[i] > 0) {
            if ($aslide[$i] > 0) {
                # ge_p1p1_to_p3(&u,&t);
                # ge_add(&t,&u,&Ai[aslide[i]/2]);
                $u = self::ge_p1p1_to_p3($t);
                $t = self::ge_add(
                    $u,
                    $Ai[(int) floor($aslide[$i] / 2)]
                );
                # } else if (aslide[i] < 0) {
            } elseif ($aslide[$i] < 0) {
                # ge_p1p1_to_p3(&u,&t);
                # ge_sub(&t,&u,&Ai[(-aslide[i])/2]);
                $u = self::ge_p1p1_to_p3($t);
                $t = self::ge_sub(
                    $u,
                    $Ai[(int) floor(-$aslide[$i] / 2)]
                );
            }
            /** @var array<int, ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp> $Bi */

            # if (bslide[i] > 0) {
            if ($bslide[$i] > 0) {
                # ge_p1p1_to_p3(&u,&t);
                # ge_madd(&t,&u,&Bi[bslide[i]/2]);
                $u = self::ge_p1p1_to_p3($t);
                /** @var int $index */
                $index = (int) floor($bslide[$i] / 2);
                /** @var ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp $thisB */
                $thisB = $Bi[$index];
                $t = self::ge_madd($t, $u, $thisB);
                # } else if (bslide[i] < 0) {
            } elseif ($bslide[$i] < 0) {
                # ge_p1p1_to_p3(&u,&t);
                # ge_msub(&t,&u,&Bi[(-bslide[i])/2]);
                $u = self::ge_p1p1_to_p3($t);

                /** @var int $index */
                $index = (int) floor(-$bslide[$i] / 2);

                /** @var ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp $thisB */
                $thisB = $Bi[$index];
                $t = self::ge_msub($t, $u, $thisB);
            }
            # ge_p1p1_to_p2(r,&t);
            $r = self::ge_p1p1_to_p2($t);
        }
        return $r;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $a
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P3
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedOperand
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_scalarmult_base($a)
    {
        /** @var array<int, int> $e */
        $e = array();
        $r = new ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1();

        for ($i = 0; $i < 32; ++$i) {
            /** @var int $dbl */
            $dbl = (int) $i << 1;
            $e[$dbl] = (int) self::chrToInt($a[$i]) & 15;
            $e[$dbl + 1] = (int) (self::chrToInt($a[$i]) >> 4) & 15;
        }

        /** @var int $carry */
        $carry = 0;
        for ($i = 0; $i < 63; ++$i) {
            $e[$i] += $carry;
            /** @var int $carry */
            $carry = $e[$i] + 8;
            /** @var int $carry */
            $carry >>= 4;
            $e[$i] -= $carry << 4;
        }

        /** @var array<int, int> $e */
        $e[63] += (int) $carry;

        $h = self::ge_p3_0();

        for ($i = 1; $i < 64; $i += 2) {
            $t = self::ge_select((int) floor($i / 2), (int) $e[$i]);
            $r = self::ge_madd($r, $h, $t);
            $h = self::ge_p1p1_to_p3($r);
        }

        $r = self::ge_p3_dbl($h);

        $s = self::ge_p1p1_to_p2($r);
        $r = self::ge_p2_dbl($s);
        $s = self::ge_p1p1_to_p2($r);
        $r = self::ge_p2_dbl($s);
        $s = self::ge_p1p1_to_p2($r);
        $r = self::ge_p2_dbl($s);

        $h = self::ge_p1p1_to_p3($r);

        for ($i = 0; $i < 64; $i += 2) {
            $t = self::ge_select($i >> 1, (int) $e[$i]);
            $r = self::ge_madd($r, $h, $t);
            $h = self::ge_p1p1_to_p3($r);
        }
        return $h;
    }

    /**
     * Calculates (ab + c) mod l
     * where l = 2^252 + 27742317777372353535851937790883648493
     *
     * @internal You should not use this directly from another application
     *
     * @param string $a
     * @param string $b
     * @param string $c
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sc_muladd($a, $b, $c)
    {
        $a0 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & self::load_3(self::substr($a, 0, 3)));
        $a1 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($a, 2, 4)) >> 5));
        $a2 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($a, 5, 3)) >> 2));
        $a3 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($a, 7, 4)) >> 7));
        $a4 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($a, 10, 4)) >> 4));
        $a5 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($a, 13, 3)) >> 1));
        $a6 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($a, 15, 4)) >> 6));
        $a7 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($a, 18, 3)) >> 3));
        $a8 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & self::load_3(self::substr($a, 21, 3)));
        $a9 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($a, 23, 4)) >> 5));
        $a10 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($a, 26, 3)) >> 2));
        $a11 = ParagonIE_Sodium_Core32_Int64::fromInt(0x1fffffff & (self::load_4(self::substr($a, 28, 4)) >> 7));
        $b0 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & self::load_3(self::substr($b, 0, 3)));
        $b1 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($b, 2, 4)) >> 5));
        $b2 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($b, 5, 3)) >> 2));
        $b3 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($b, 7, 4)) >> 7));
        $b4 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($b, 10, 4)) >> 4));
        $b5 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($b, 13, 3)) >> 1));
        $b6 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($b, 15, 4)) >> 6));
        $b7 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($b, 18, 3)) >> 3));
        $b8 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & self::load_3(self::substr($b, 21, 3)));
        $b9 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($b, 23, 4)) >> 5));
        $b10 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($b, 26, 3)) >> 2));
        $b11 = ParagonIE_Sodium_Core32_Int64::fromInt(0x1fffffff & (self::load_4(self::substr($b, 28, 4)) >> 7));
        $c0 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & self::load_3(self::substr($c, 0, 3)));
        $c1 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($c, 2, 4)) >> 5));
        $c2 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($c, 5, 3)) >> 2));
        $c3 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($c, 7, 4)) >> 7));
        $c4 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($c, 10, 4)) >> 4));
        $c5 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($c, 13, 3)) >> 1));
        $c6 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($c, 15, 4)) >> 6));
        $c7 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($c, 18, 3)) >> 3));
        $c8 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & self::load_3(self::substr($c, 21, 3)));
        $c9 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($c, 23, 4)) >> 5));
        $c10 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($c, 26, 3)) >> 2));
        $c11 = ParagonIE_Sodium_Core32_Int64::fromInt(0x1fffffff & (self::load_4(self::substr($c, 28, 4)) >> 7));

        /* Can't really avoid the pyramid here: */
        /**
         * @var ParagonIE_Sodium_Core32_Int64 $s0
         * @var ParagonIE_Sodium_Core32_Int64 $s1
         * @var ParagonIE_Sodium_Core32_Int64 $s2
         * @var ParagonIE_Sodium_Core32_Int64 $s3
         * @var ParagonIE_Sodium_Core32_Int64 $s4
         * @var ParagonIE_Sodium_Core32_Int64 $s5
         * @var ParagonIE_Sodium_Core32_Int64 $s6
         * @var ParagonIE_Sodium_Core32_Int64 $s7
         * @var ParagonIE_Sodium_Core32_Int64 $s8
         * @var ParagonIE_Sodium_Core32_Int64 $s9
         * @var ParagonIE_Sodium_Core32_Int64 $s10
         * @var ParagonIE_Sodium_Core32_Int64 $s11
         * @var ParagonIE_Sodium_Core32_Int64 $s12
         * @var ParagonIE_Sodium_Core32_Int64 $s13
         * @var ParagonIE_Sodium_Core32_Int64 $s14
         * @var ParagonIE_Sodium_Core32_Int64 $s15
         * @var ParagonIE_Sodium_Core32_Int64 $s16
         * @var ParagonIE_Sodium_Core32_Int64 $s17
         * @var ParagonIE_Sodium_Core32_Int64 $s18
         * @var ParagonIE_Sodium_Core32_Int64 $s19
         * @var ParagonIE_Sodium_Core32_Int64 $s20
         * @var ParagonIE_Sodium_Core32_Int64 $s21
         * @var ParagonIE_Sodium_Core32_Int64 $s22
         * @var ParagonIE_Sodium_Core32_Int64 $s23
         */

        $s0 = $c0->addInt64($a0->mulInt64($b0, 24));
        $s1 = $c1->addInt64($a0->mulInt64($b1, 24))->addInt64($a1->mulInt64($b0, 24));
        $s2 = $c2->addInt64($a0->mulInt64($b2, 24))->addInt64($a1->mulInt64($b1, 24))->addInt64($a2->mulInt64($b0, 24));
        $s3 = $c3->addInt64($a0->mulInt64($b3, 24))->addInt64($a1->mulInt64($b2, 24))->addInt64($a2->mulInt64($b1, 24))
                 ->addInt64($a3->mulInt64($b0, 24));
        $s4 = $c4->addInt64($a0->mulInt64($b4, 24))->addInt64($a1->mulInt64($b3, 24))->addInt64($a2->mulInt64($b2, 24))
                 ->addInt64($a3->mulInt64($b1, 24))->addInt64($a4->mulInt64($b0, 24));
        $s5 = $c5->addInt64($a0->mulInt64($b5, 24))->addInt64($a1->mulInt64($b4, 24))->addInt64($a2->mulInt64($b3, 24))
                 ->addInt64($a3->mulInt64($b2, 24))->addInt64($a4->mulInt64($b1, 24))->addInt64($a5->mulInt64($b0, 24));
        $s6 = $c6->addInt64($a0->mulInt64($b6, 24))->addInt64($a1->mulInt64($b5, 24))->addInt64($a2->mulInt64($b4, 24))
                 ->addInt64($a3->mulInt64($b3, 24))->addInt64($a4->mulInt64($b2, 24))->addInt64($a5->mulInt64($b1, 24))
                 ->addInt64($a6->mulInt64($b0, 24));
        $s7 = $c7->addInt64($a0->mulInt64($b7, 24))->addInt64($a1->mulInt64($b6, 24))->addInt64($a2->mulInt64($b5, 24))
                 ->addInt64($a3->mulInt64($b4, 24))->addInt64($a4->mulInt64($b3, 24))->addInt64($a5->mulInt64($b2, 24))
                 ->addInt64($a6->mulInt64($b1, 24))->addInt64($a7->mulInt64($b0, 24));
        $s8 = $c8->addInt64($a0->mulInt64($b8, 24))->addInt64($a1->mulInt64($b7, 24))->addInt64($a2->mulInt64($b6, 24))
                 ->addInt64($a3->mulInt64($b5, 24))->addInt64($a4->mulInt64($b4, 24))->addInt64($a5->mulInt64($b3, 24))
                 ->addInt64($a6->mulInt64($b2, 24))->addInt64($a7->mulInt64($b1, 24))->addInt64($a8->mulInt64($b0, 24));
        $s9 = $c9->addInt64($a0->mulInt64($b9, 24))->addInt64($a1->mulInt64($b8, 24))->addInt64($a2->mulInt64($b7, 24))
                 ->addInt64($a3->mulInt64($b6, 24))->addInt64($a4->mulInt64($b5, 24))->addInt64($a5->mulInt64($b4, 24))
                 ->addInt64($a6->mulInt64($b3, 24))->addInt64($a7->mulInt64($b2, 24))->addInt64($a8->mulInt64($b1, 24))
                 ->addInt64($a9->mulInt64($b0, 24));
        $s10 = $c10->addInt64($a0->mulInt64($b10, 24))->addInt64($a1->mulInt64($b9, 24))->addInt64($a2->mulInt64($b8, 24))
                   ->addInt64($a3->mulInt64($b7, 24))->addInt64($a4->mulInt64($b6, 24))->addInt64($a5->mulInt64($b5, 24))
                   ->addInt64($a6->mulInt64($b4, 24))->addInt64($a7->mulInt64($b3, 24))->addInt64($a8->mulInt64($b2, 24))
                   ->addInt64($a9->mulInt64($b1, 24))->addInt64($a10->mulInt64($b0, 24));
        $s11 = $c11->addInt64($a0->mulInt64($b11, 24))->addInt64($a1->mulInt64($b10, 24))->addInt64($a2->mulInt64($b9, 24))
                   ->addInt64($a3->mulInt64($b8, 24))->addInt64($a4->mulInt64($b7, 24))->addInt64($a5->mulInt64($b6, 24))
                   ->addInt64($a6->mulInt64($b5, 24))->addInt64($a7->mulInt64($b4, 24))->addInt64($a8->mulInt64($b3, 24))
                   ->addInt64($a9->mulInt64($b2, 24))->addInt64($a10->mulInt64($b1, 24))->addInt64($a11->mulInt64($b0, 24));
        $s12 = $a1->mulInt64($b11, 24)->addInt64($a2->mulInt64($b10, 24))->addInt64($a3->mulInt64($b9, 24))
                  ->addInt64($a4->mulInt64($b8, 24))->addInt64($a5->mulInt64($b7, 24))->addInt64($a6->mulInt64($b6, 24))
                  ->addInt64($a7->mulInt64($b5, 24))->addInt64($a8->mulInt64($b4, 24))->addInt64($a9->mulInt64($b3, 24))
                  ->addInt64($a10->mulInt64($b2, 24))->addInt64($a11->mulInt64($b1, 24));
        $s13 = $a2->mulInt64($b11, 24)->addInt64($a3->mulInt64($b10, 24))->addInt64($a4->mulInt64($b9, 24))
                  ->addInt64($a5->mulInt64($b8, 24))->addInt64($a6->mulInt64($b7, 24))->addInt64($a7->mulInt64($b6, 24))
                  ->addInt64($a8->mulInt64($b5, 24))->addInt64($a9->mulInt64($b4, 24))->addInt64($a10->mulInt64($b3, 24))
                  ->addInt64($a11->mulInt64($b2, 24));
        $s14 = $a3->mulInt64($b11, 24)->addInt64($a4->mulInt64($b10, 24))->addInt64($a5->mulInt64($b9, 24))
                  ->addInt64($a6->mulInt64($b8, 24))->addInt64($a7->mulInt64($b7, 24))->addInt64($a8->mulInt64($b6, 24))
                  ->addInt64($a9->mulInt64($b5, 24))->addInt64($a10->mulInt64($b4, 24))->addInt64($a11->mulInt64($b3, 24));
        $s15 = $a4->mulInt64($b11, 24)->addInt64($a5->mulInt64($b10, 24))->addInt64($a6->mulInt64($b9, 24))
                  ->addInt64($a7->mulInt64($b8, 24))->addInt64($a8->mulInt64($b7, 24))->addInt64($a9->mulInt64($b6, 24))
                  ->addInt64($a10->mulInt64($b5, 24))->addInt64($a11->mulInt64($b4, 24));
        $s16 = $a5->mulInt64($b11, 24)->addInt64($a6->mulInt64($b10, 24))->addInt64($a7->mulInt64($b9, 24))
                  ->addInt64($a8->mulInt64($b8, 24))->addInt64($a9->mulInt64($b7, 24))->addInt64($a10->mulInt64($b6, 24))
                  ->addInt64($a11->mulInt64($b5, 24));
        $s17 = $a6->mulInt64($b11, 24)->addInt64($a7->mulInt64($b10, 24))->addInt64($a8->mulInt64($b9, 24))
                  ->addInt64($a9->mulInt64($b8, 24))->addInt64($a10->mulInt64($b7, 24))->addInt64($a11->mulInt64($b6, 24));
        $s18 = $a7->mulInt64($b11, 24)->addInt64($a8->mulInt64($b10, 24))->addInt64($a9->mulInt64($b9, 24))
                  ->addInt64($a10->mulInt64($b8, 24))->addInt64($a11->mulInt64($b7, 24));
        $s19 = $a8->mulInt64($b11, 24)->addInt64($a9->mulInt64($b10, 24))->addInt64($a10->mulInt64($b9, 24))
                  ->addInt64($a11->mulInt64($b8, 24));
        $s20 = $a9->mulInt64($b11, 24)->addInt64($a10->mulInt64($b10, 24))->addInt64($a11->mulInt64($b9, 24));
        $s21 = $a10->mulInt64($b11, 24)->addInt64($a11->mulInt64($b10, 24));
        $s22 = $a11->mulInt64($b11, 24);
        $s23 = new ParagonIE_Sodium_Core32_Int64();

        $carry0 = $s0->addInt(1 << 20)->shiftRight(21);
        $s1 = $s1->addInt64($carry0);
        $s0 = $s0->subInt64($carry0->shiftLeft(21));
        $carry2 = $s2->addInt(1 << 20)->shiftRight(21);
        $s3 = $s3->addInt64($carry2);
        $s2 = $s2->subInt64($carry2->shiftLeft(21));
        $carry4 = $s4->addInt(1 << 20)->shiftRight(21);
        $s5 = $s5->addInt64($carry4);
        $s4 = $s4->subInt64($carry4->shiftLeft(21));
        $carry6 = $s6->addInt(1 << 20)->shiftRight(21);
        $s7 = $s7->addInt64($carry6);
        $s6 = $s6->subInt64($carry6->shiftLeft(21));
        $carry8 = $s8->addInt(1 << 20)->shiftRight(21);
        $s9 = $s9->addInt64($carry8);
        $s8 = $s8->subInt64($carry8->shiftLeft(21));
        $carry10 = $s10->addInt(1 << 20)->shiftRight(21);
        $s11 = $s11->addInt64($carry10);
        $s10 = $s10->subInt64($carry10->shiftLeft(21));
        $carry12 = $s12->addInt(1 << 20)->shiftRight(21);
        $s13 = $s13->addInt64($carry12);
        $s12 = $s12->subInt64($carry12->shiftLeft(21));
        $carry14 = $s14->addInt(1 << 20)->shiftRight(21);
        $s15 = $s15->addInt64($carry14);
        $s14 = $s14->subInt64($carry14->shiftLeft(21));
        $carry16 = $s16->addInt(1 << 20)->shiftRight(21);
        $s17 = $s17->addInt64($carry16);
        $s16 = $s16->subInt64($carry16->shiftLeft(21));
        $carry18 = $s18->addInt(1 << 20)->shiftRight(21);
        $s19 = $s19->addInt64($carry18);
        $s18 = $s18->subInt64($carry18->shiftLeft(21));
        $carry20 = $s20->addInt(1 << 20)->shiftRight(21);
        $s21 = $s21->addInt64($carry20);
        $s20 = $s20->subInt64($carry20->shiftLeft(21));
        $carry22 = $s22->addInt(1 << 20)->shiftRight(21);
        $s23 = $s23->addInt64($carry22);
        $s22 = $s22->subInt64($carry22->shiftLeft(21));

        $carry1 = $s1->addInt(1 << 20)->shiftRight(21);
        $s2 = $s2->addInt64($carry1);
        $s1 = $s1->subInt64($carry1->shiftLeft(21));
        $carry3 = $s3->addInt(1 << 20)->shiftRight(21);
        $s4 = $s4->addInt64($carry3);
        $s3 = $s3->subInt64($carry3->shiftLeft(21));
        $carry5 = $s5->addInt(1 << 20)->shiftRight(21);
        $s6 = $s6->addInt64($carry5);
        $s5 = $s5->subInt64($carry5->shiftLeft(21));
        $carry7 = $s7->addInt(1 << 20)->shiftRight(21);
        $s8 = $s8->addInt64($carry7);
        $s7 = $s7->subInt64($carry7->shiftLeft(21));
        $carry9 = $s9->addInt(1 << 20)->shiftRight(21);
        $s10 = $s10->addInt64($carry9);
        $s9 = $s9->subInt64($carry9->shiftLeft(21));
        $carry11 = $s11->addInt(1 << 20)->shiftRight(21);
        $s12 = $s12->addInt64($carry11);
        $s11 = $s11->subInt64($carry11->shiftLeft(21));
        $carry13 = $s13->addInt(1 << 20)->shiftRight(21);
        $s14 = $s14->addInt64($carry13);
        $s13 = $s13->subInt64($carry13->shiftLeft(21));
        $carry15 = $s15->addInt(1 << 20)->shiftRight(21);
        $s16 = $s16->addInt64($carry15);
        $s15 = $s15->subInt64($carry15->shiftLeft(21));
        $carry17 = $s17->addInt(1 << 20)->shiftRight(21);
        $s18 = $s18->addInt64($carry17);
        $s17 = $s17->subInt64($carry17->shiftLeft(21));
        $carry19 = $s19->addInt(1 << 20)->shiftRight(21);
        $s20 = $s20->addInt64($carry19);
        $s19 = $s19->subInt64($carry19->shiftLeft(21));
        $carry21 = $s21->addInt(1 << 20)->shiftRight(21);
        $s22 = $s22->addInt64($carry21);
        $s21 = $s21->subInt64($carry21->shiftLeft(21));

        $s11 = $s11->addInt64($s23->mulInt(666643, 20));
        $s12 = $s12->addInt64($s23->mulInt(470296, 19));
        $s13 = $s13->addInt64($s23->mulInt(654183, 20));
        $s14 = $s14->subInt64($s23->mulInt(997805, 20));
        $s15 = $s15->addInt64($s23->mulInt(136657, 18));
        $s16 = $s16->subInt64($s23->mulInt(683901, 20));

        $s10 = $s10->addInt64($s22->mulInt(666643, 20));
        $s11 = $s11->addInt64($s22->mulInt(470296, 19));
        $s12 = $s12->addInt64($s22->mulInt(654183, 20));
        $s13 = $s13->subInt64($s22->mulInt(997805, 20));
        $s14 = $s14->addInt64($s22->mulInt(136657, 18));
        $s15 = $s15->subInt64($s22->mulInt(683901, 20));

        $s9  =  $s9->addInt64($s21->mulInt(666643, 20));
        $s10 = $s10->addInt64($s21->mulInt(470296, 19));
        $s11 = $s11->addInt64($s21->mulInt(654183, 20));
        $s12 = $s12->subInt64($s21->mulInt(997805, 20));
        $s13 = $s13->addInt64($s21->mulInt(136657, 18));
        $s14 = $s14->subInt64($s21->mulInt(683901, 20));

        $s8  =  $s8->addInt64($s20->mulInt(666643, 20));
        $s9  =  $s9->addInt64($s20->mulInt(470296, 19));
        $s10 = $s10->addInt64($s20->mulInt(654183, 20));
        $s11 = $s11->subInt64($s20->mulInt(997805, 20));
        $s12 = $s12->addInt64($s20->mulInt(136657, 18));
        $s13 = $s13->subInt64($s20->mulInt(683901, 20));

        $s7  =  $s7->addInt64($s19->mulInt(666643, 20));
        $s8  =  $s8->addInt64($s19->mulInt(470296, 19));
        $s9  =  $s9->addInt64($s19->mulInt(654183, 20));
        $s10 = $s10->subInt64($s19->mulInt(997805, 20));
        $s11 = $s11->addInt64($s19->mulInt(136657, 18));
        $s12 = $s12->subInt64($s19->mulInt(683901, 20));

        $s6  =  $s6->addInt64($s18->mulInt(666643, 20));
        $s7  =  $s7->addInt64($s18->mulInt(470296, 19));
        $s8  =  $s8->addInt64($s18->mulInt(654183, 20));
        $s9  =  $s9->subInt64($s18->mulInt(997805, 20));
        $s10 = $s10->addInt64($s18->mulInt(136657, 18));
        $s11 = $s11->subInt64($s18->mulInt(683901, 20));

        $carry6 = $s6->addInt(1 << 20)->shiftRight(21);
        $s7 = $s7->addInt64($carry6);
        $s6 = $s6->subInt64($carry6->shiftLeft(21));
        $carry8 = $s8->addInt(1 << 20)->shiftRight(21);
        $s9 = $s9->addInt64($carry8);
        $s8 = $s8->subInt64($carry8->shiftLeft(21));
        $carry10 = $s10->addInt(1 << 20)->shiftRight(21);
        $s11 = $s11->addInt64($carry10);
        $s10 = $s10->subInt64($carry10->shiftLeft(21));
        $carry12 = $s12->addInt(1 << 20)->shiftRight(21);
        $s13 = $s13->addInt64($carry12);
        $s12 = $s12->subInt64($carry12->shiftLeft(21));
        $carry14 = $s14->addInt(1 << 20)->shiftRight(21);
        $s15 = $s15->addInt64($carry14);
        $s14 = $s14->subInt64($carry14->shiftLeft(21));
        $carry16 = $s16->addInt(1 << 20)->shiftRight(21);
        $s17 = $s17->addInt64($carry16);
        $s16 = $s16->subInt64($carry16->shiftLeft(21));

        $carry7 = $s7->addInt(1 << 20)->shiftRight(21);
        $s8 = $s8->addInt64($carry7);
        $s7 = $s7->subInt64($carry7->shiftLeft(21));
        $carry9 = $s9->addInt(1 << 20)->shiftRight(21);
        $s10 = $s10->addInt64($carry9);
        $s9 = $s9->subInt64($carry9->shiftLeft(21));
        $carry11 = $s11->addInt(1 << 20)->shiftRight(21);
        $s12 = $s12->addInt64($carry11);
        $s11 = $s11->subInt64($carry11->shiftLeft(21));
        $carry13 = $s13->addInt(1 << 20)->shiftRight(21);
        $s14 = $s14->addInt64($carry13);
        $s13 = $s13->subInt64($carry13->shiftLeft(21));
        $carry15 = $s15->addInt(1 << 20)->shiftRight(21);
        $s16 = $s16->addInt64($carry15);
        $s15 = $s15->subInt64($carry15->shiftLeft(21));

        $s5  =  $s5->addInt64($s17->mulInt(666643, 20));
        $s6  =  $s6->addInt64($s17->mulInt(470296, 19));
        $s7  =  $s7->addInt64($s17->mulInt(654183, 20));
        $s8  =  $s8->subInt64($s17->mulInt(997805, 20));
        $s9  =  $s9->addInt64($s17->mulInt(136657, 18));
        $s10 = $s10->subInt64($s17->mulInt(683901, 20));

        $s4  =  $s4->addInt64($s16->mulInt(666643, 20));
        $s5  =  $s5->addInt64($s16->mulInt(470296, 19));
        $s6  =  $s6->addInt64($s16->mulInt(654183, 20));
        $s7  =  $s7->subInt64($s16->mulInt(997805, 20));
        $s8  =  $s8->addInt64($s16->mulInt(136657, 18));
        $s9  =  $s9->subInt64($s16->mulInt(683901, 20));

        $s3  =  $s3->addInt64($s15->mulInt(666643, 20));
        $s4  =  $s4->addInt64($s15->mulInt(470296, 19));
        $s5  =  $s5->addInt64($s15->mulInt(654183, 20));
        $s6  =  $s6->subInt64($s15->mulInt(997805, 20));
        $s7  =  $s7->addInt64($s15->mulInt(136657, 18));
        $s8  =  $s8->subInt64($s15->mulInt(683901, 20));

        $s2  =  $s2->addInt64($s14->mulInt(666643, 20));
        $s3  =  $s3->addInt64($s14->mulInt(470296, 19));
        $s4  =  $s4->addInt64($s14->mulInt(654183, 20));
        $s5  =  $s5->subInt64($s14->mulInt(997805, 20));
        $s6  =  $s6->addInt64($s14->mulInt(136657, 18));
        $s7  =  $s7->subInt64($s14->mulInt(683901, 20));

        $s1  =  $s1->addInt64($s13->mulInt(666643, 20));
        $s2  =  $s2->addInt64($s13->mulInt(470296, 19));
        $s3  =  $s3->addInt64($s13->mulInt(654183, 20));
        $s4  =  $s4->subInt64($s13->mulInt(997805, 20));
        $s5  =  $s5->addInt64($s13->mulInt(136657, 18));
        $s6  =  $s6->subInt64($s13->mulInt(683901, 20));

        $s0  =  $s0->addInt64($s12->mulInt(666643, 20));
        $s1  =  $s1->addInt64($s12->mulInt(470296, 19));
        $s2  =  $s2->addInt64($s12->mulInt(654183, 20));
        $s3  =  $s3->subInt64($s12->mulInt(997805, 20));
        $s4  =  $s4->addInt64($s12->mulInt(136657, 18));
        $s5  =  $s5->subInt64($s12->mulInt(683901, 20));
        $s12 = new ParagonIE_Sodium_Core32_Int64();

        $carry0 = $s0->addInt(1 << 20)->shiftRight(21);
        $s1 = $s1->addInt64($carry0);
        $s0 = $s0->subInt64($carry0->shiftLeft(21));
        $carry2 = $s2->addInt(1 << 20)->shiftRight(21);
        $s3 = $s3->addInt64($carry2);
        $s2 = $s2->subInt64($carry2->shiftLeft(21));
        $carry4 = $s4->addInt(1 << 20)->shiftRight(21);
        $s5 = $s5->addInt64($carry4);
        $s4 = $s4->subInt64($carry4->shiftLeft(21));
        $carry6 = $s6->addInt(1 << 20)->shiftRight(21);
        $s7 = $s7->addInt64($carry6);
        $s6 = $s6->subInt64($carry6->shiftLeft(21));
        $carry8 = $s8->addInt(1 << 20)->shiftRight(21);
        $s9 = $s9->addInt64($carry8);
        $s8 = $s8->subInt64($carry8->shiftLeft(21));
        $carry10 = $s10->addInt(1 << 20)->shiftRight(21);
        $s11 = $s11->addInt64($carry10);
        $s10 = $s10->subInt64($carry10->shiftLeft(21));

        $carry1 = $s1->addInt(1 << 20)->shiftRight(21);
        $s2 = $s2->addInt64($carry1);
        $s1 = $s1->subInt64($carry1->shiftLeft(21));
        $carry3 = $s3->addInt(1 << 20)->shiftRight(21);
        $s4 = $s4->addInt64($carry3);
        $s3 = $s3->subInt64($carry3->shiftLeft(21));
        $carry5 = $s5->addInt(1 << 20)->shiftRight(21);
        $s6 = $s6->addInt64($carry5);
        $s5 = $s5->subInt64($carry5->shiftLeft(21));
        $carry7 = $s7->addInt(1 << 20)->shiftRight(21);
        $s8 = $s8->addInt64($carry7);
        $s7 = $s7->subInt64($carry7->shiftLeft(21));
        $carry9 = $s9->addInt(1 << 20)->shiftRight(21);
        $s10 = $s10->addInt64($carry9);
        $s9 = $s9->subInt64($carry9->shiftLeft(21));
        $carry11 = $s11->addInt(1 << 20)->shiftRight(21);
        $s12 = $s12->addInt64($carry11);
        $s11 = $s11->subInt64($carry11->shiftLeft(21));

        $s0  =  $s0->addInt64($s12->mulInt(666643, 20));
        $s1  =  $s1->addInt64($s12->mulInt(470296, 19));
        $s2  =  $s2->addInt64($s12->mulInt(654183, 20));
        $s3  =  $s3->subInt64($s12->mulInt(997805, 20));
        $s4  =  $s4->addInt64($s12->mulInt(136657, 18));
        $s5  =  $s5->subInt64($s12->mulInt(683901, 20));
        $s12 = new ParagonIE_Sodium_Core32_Int64();

        $carry0 = $s0->shiftRight(21);
        $s1 = $s1->addInt64($carry0);
        $s0 = $s0->subInt64($carry0->shiftLeft(21));
        $carry1 = $s1->shiftRight(21);
        $s2 = $s2->addInt64($carry1);
        $s1 = $s1->subInt64($carry1->shiftLeft(21));
        $carry2 = $s2->shiftRight(21);
        $s3 = $s3->addInt64($carry2);
        $s2 = $s2->subInt64($carry2->shiftLeft(21));
        $carry3 = $s3->shiftRight(21);
        $s4 = $s4->addInt64($carry3);
        $s3 = $s3->subInt64($carry3->shiftLeft(21));
        $carry4 = $s4->shiftRight(21);
        $s5 = $s5->addInt64($carry4);
        $s4 = $s4->subInt64($carry4->shiftLeft(21));
        $carry5 = $s5->shiftRight(21);
        $s6 = $s6->addInt64($carry5);
        $s5 = $s5->subInt64($carry5->shiftLeft(21));
        $carry6 = $s6->shiftRight(21);
        $s7 = $s7->addInt64($carry6);
        $s6 = $s6->subInt64($carry6->shiftLeft(21));
        $carry7 = $s7->shiftRight(21);
        $s8 = $s8->addInt64($carry7);
        $s7 = $s7->subInt64($carry7->shiftLeft(21));
        $carry8 = $s8->shiftRight(21);
        $s9 = $s9->addInt64($carry8);
        $s8 = $s8->subInt64($carry8->shiftLeft(21));
        $carry9 = $s9->shiftRight(21);
        $s10 = $s10->addInt64($carry9);
        $s9 = $s9->subInt64($carry9->shiftLeft(21));
        $carry10 = $s10->shiftRight(21);
        $s11 = $s11->addInt64($carry10);
        $s10 = $s10->subInt64($carry10->shiftLeft(21));
        $carry11 = $s11->shiftRight(21);
        $s12 = $s12->addInt64($carry11);
        $s11 = $s11->subInt64($carry11->shiftLeft(21));

        $s0  =  $s0->addInt64($s12->mulInt(666643, 20));
        $s1  =  $s1->addInt64($s12->mulInt(470296, 19));
        $s2  =  $s2->addInt64($s12->mulInt(654183, 20));
        $s3  =  $s3->subInt64($s12->mulInt(997805, 20));
        $s4  =  $s4->addInt64($s12->mulInt(136657, 18));
        $s5  =  $s5->subInt64($s12->mulInt(683901, 20));

        $carry0 = $s0->shiftRight(21);
        $s1 = $s1->addInt64($carry0);
        $s0 = $s0->subInt64($carry0->shiftLeft(21));
        $carry1 = $s1->shiftRight(21);
        $s2 = $s2->addInt64($carry1);
        $s1 = $s1->subInt64($carry1->shiftLeft(21));
        $carry2 = $s2->shiftRight(21);
        $s3 = $s3->addInt64($carry2);
        $s2 = $s2->subInt64($carry2->shiftLeft(21));
        $carry3 = $s3->shiftRight(21);
        $s4 = $s4->addInt64($carry3);
        $s3 = $s3->subInt64($carry3->shiftLeft(21));
        $carry4 = $s4->shiftRight(21);
        $s5 = $s5->addInt64($carry4);
        $s4 = $s4->subInt64($carry4->shiftLeft(21));
        $carry5 = $s5->shiftRight(21);
        $s6 = $s6->addInt64($carry5);
        $s5 = $s5->subInt64($carry5->shiftLeft(21));
        $carry6 = $s6->shiftRight(21);
        $s7 = $s7->addInt64($carry6);
        $s6 = $s6->subInt64($carry6->shiftLeft(21));
        $carry7 = $s7->shiftRight(21);
        $s8 = $s8->addInt64($carry7);
        $s7 = $s7->subInt64($carry7->shiftLeft(21));
        $carry8 = $s10->shiftRight(21);
        $s9 = $s9->addInt64($carry8);
        $s8 = $s8->subInt64($carry8->shiftLeft(21));
        $carry9 = $s9->shiftRight(21);
        $s10 = $s10->addInt64($carry9);
        $s9 = $s9->subInt64($carry9->shiftLeft(21));
        $carry10 = $s10->shiftRight(21);
        $s11 = $s11->addInt64($carry10);
        $s10 = $s10->subInt64($carry10->shiftLeft(21));

        $S0  =  $s0->toInt();
        $S1  =  $s1->toInt();
        $S2  =  $s2->toInt();
        $S3  =  $s3->toInt();
        $S4  =  $s4->toInt();
        $S5  =  $s5->toInt();
        $S6  =  $s6->toInt();
        $S7  =  $s7->toInt();
        $S8  =  $s8->toInt();
        $S9  =  $s9->toInt();
        $S10 = $s10->toInt();
        $S11 = $s11->toInt();

        /**
         * @var array<int, int>
         */
        $arr = array(
            (int) (0xff & ($S0 >> 0)),
            (int) (0xff & ($S0 >> 8)),
            (int) (0xff & (($S0 >> 16) | ($S1 << 5))),
            (int) (0xff & ($S1 >> 3)),
            (int) (0xff & ($S1 >> 11)),
            (int) (0xff & (($S1 >> 19) | ($S2 << 2))),
            (int) (0xff & ($S2 >> 6)),
            (int) (0xff & (($S2 >> 14) | ($S3 << 7))),
            (int) (0xff & ($S3 >> 1)),
            (int) (0xff & ($S3 >> 9)),
            (int) (0xff & (($S3 >> 17) | ($S4 << 4))),
            (int) (0xff & ($S4 >> 4)),
            (int) (0xff & ($S4 >> 12)),
            (int) (0xff & (($S4 >> 20) | ($S5 << 1))),
            (int) (0xff & ($S5 >> 7)),
            (int) (0xff & (($S5 >> 15) | ($S6 << 6))),
            (int) (0xff & ($S6 >> 2)),
            (int) (0xff & ($S6 >> 10)),
            (int) (0xff & (($S6 >> 18) | ($S7 << 3))),
            (int) (0xff & ($S7 >> 5)),
            (int) (0xff & ($S7 >> 13)),
            (int) (0xff & ($S8 >> 0)),
            (int) (0xff & ($S8 >> 8)),
            (int) (0xff & (($S8 >> 16) | ($S9 << 5))),
            (int) (0xff & ($S9 >> 3)),
            (int) (0xff & ($S9 >> 11)),
            (int) (0xff & (($S9 >> 19) | ($S10 << 2))),
            (int) (0xff & ($S10 >> 6)),
            (int) (0xff & (($S10 >> 14) | ($S11 << 7))),
            (int) (0xff & ($S11 >> 1)),
            (int) (0xff & ($S11 >> 9)),
            (int) (0xff & ($S11 >> 17))
        );
        return self::intArrayToString($arr);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $s
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sc_reduce($s)
    {
        /**
         * @var ParagonIE_Sodium_Core32_Int64 $s0
         * @var ParagonIE_Sodium_Core32_Int64 $s1
         * @var ParagonIE_Sodium_Core32_Int64 $s2
         * @var ParagonIE_Sodium_Core32_Int64 $s3
         * @var ParagonIE_Sodium_Core32_Int64 $s4
         * @var ParagonIE_Sodium_Core32_Int64 $s5
         * @var ParagonIE_Sodium_Core32_Int64 $s6
         * @var ParagonIE_Sodium_Core32_Int64 $s7
         * @var ParagonIE_Sodium_Core32_Int64 $s8
         * @var ParagonIE_Sodium_Core32_Int64 $s9
         * @var ParagonIE_Sodium_Core32_Int64 $s10
         * @var ParagonIE_Sodium_Core32_Int64 $s11
         * @var ParagonIE_Sodium_Core32_Int64 $s12
         * @var ParagonIE_Sodium_Core32_Int64 $s13
         * @var ParagonIE_Sodium_Core32_Int64 $s14
         * @var ParagonIE_Sodium_Core32_Int64 $s15
         * @var ParagonIE_Sodium_Core32_Int64 $s16
         * @var ParagonIE_Sodium_Core32_Int64 $s17
         * @var ParagonIE_Sodium_Core32_Int64 $s18
         * @var ParagonIE_Sodium_Core32_Int64 $s19
         * @var ParagonIE_Sodium_Core32_Int64 $s20
         * @var ParagonIE_Sodium_Core32_Int64 $s21
         * @var ParagonIE_Sodium_Core32_Int64 $s22
         * @var ParagonIE_Sodium_Core32_Int64 $s23
         */
        $s0 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & self::load_3(self::substr($s, 0, 3)));
        $s1 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($s, 2, 4)) >> 5));
        $s2 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($s, 5, 3)) >> 2));
        $s3 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($s, 7, 4)) >> 7));
        $s4 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($s, 10, 4)) >> 4));
        $s5 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($s, 13, 3)) >> 1));
        $s6 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($s, 15, 4)) >> 6));
        $s7 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($s, 18, 4)) >> 3));
        $s8 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & self::load_3(self::substr($s, 21, 3)));
        $s9 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($s, 23, 4)) >> 5));
        $s10 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($s, 26, 3)) >> 2));
        $s11 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($s, 28, 4)) >> 7));
        $s12 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($s, 31, 4)) >> 4));
        $s13 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($s, 34, 3)) >> 1));
        $s14 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($s, 36, 4)) >> 6));
        $s15 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($s, 39, 4)) >> 3));
        $s16 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & self::load_3(self::substr($s, 42, 3)));
        $s17 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($s, 44, 4)) >> 5));
        $s18 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($s, 47, 3)) >> 2));
        $s19 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($s, 49, 4)) >> 7));
        $s20 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($s, 52, 4)) >> 4));
        $s21 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_3(self::substr($s, 55, 3)) >> 1));
        $s22 = ParagonIE_Sodium_Core32_Int64::fromInt(2097151 & (self::load_4(self::substr($s, 57, 4)) >> 6));
        $s23 = ParagonIE_Sodium_Core32_Int64::fromInt(0x1fffffff & (self::load_4(self::substr($s, 60, 4)) >> 3));

        $s11 = $s11->addInt64($s23->mulInt(666643, 20));
        $s12 = $s12->addInt64($s23->mulInt(470296, 19));
        $s13 = $s13->addInt64($s23->mulInt(654183, 20));
        $s14 = $s14->subInt64($s23->mulInt(997805, 20));
        $s15 = $s15->addInt64($s23->mulInt(136657, 18));
        $s16 = $s16->subInt64($s23->mulInt(683901, 20));

        $s10 = $s10->addInt64($s22->mulInt(666643, 20));
        $s11 = $s11->addInt64($s22->mulInt(470296, 19));
        $s12 = $s12->addInt64($s22->mulInt(654183, 20));
        $s13 = $s13->subInt64($s22->mulInt(997805, 20));
        $s14 = $s14->addInt64($s22->mulInt(136657, 18));
        $s15 = $s15->subInt64($s22->mulInt(683901, 20));

        $s9  =  $s9->addInt64($s21->mulInt(666643, 20));
        $s10 = $s10->addInt64($s21->mulInt(470296, 19));
        $s11 = $s11->addInt64($s21->mulInt(654183, 20));
        $s12 = $s12->subInt64($s21->mulInt(997805, 20));
        $s13 = $s13->addInt64($s21->mulInt(136657, 18));
        $s14 = $s14->subInt64($s21->mulInt(683901, 20));

        $s8  =  $s8->addInt64($s20->mulInt(666643, 20));
        $s9  =  $s9->addInt64($s20->mulInt(470296, 19));
        $s10 = $s10->addInt64($s20->mulInt(654183, 20));
        $s11 = $s11->subInt64($s20->mulInt(997805, 20));
        $s12 = $s12->addInt64($s20->mulInt(136657, 18));
        $s13 = $s13->subInt64($s20->mulInt(683901, 20));

        $s7  =  $s7->addInt64($s19->mulInt(666643, 20));
        $s8  =  $s8->addInt64($s19->mulInt(470296, 19));
        $s9  =  $s9->addInt64($s19->mulInt(654183, 20));
        $s10 = $s10->subInt64($s19->mulInt(997805, 20));
        $s11 = $s11->addInt64($s19->mulInt(136657, 18));
        $s12 = $s12->subInt64($s19->mulInt(683901, 20));

        $s6  =  $s6->addInt64($s18->mulInt(666643, 20));
        $s7  =  $s7->addInt64($s18->mulInt(470296, 19));
        $s8  =  $s8->addInt64($s18->mulInt(654183, 20));
        $s9  =  $s9->subInt64($s18->mulInt(997805, 20));
        $s10 = $s10->addInt64($s18->mulInt(136657, 18));
        $s11 = $s11->subInt64($s18->mulInt(683901, 20));

        $carry6 = $s6->addInt(1 << 20)->shiftRight(21);
        $s7 = $s7->addInt64($carry6);
        $s6 = $s6->subInt64($carry6->shiftLeft(21));
        $carry8 = $s8->addInt(1 << 20)->shiftRight(21);
        $s9 = $s9->addInt64($carry8);
        $s8 = $s8->subInt64($carry8->shiftLeft(21));
        $carry10 = $s10->addInt(1 << 20)->shiftRight(21);
        $s11 = $s11->addInt64($carry10);
        $s10 = $s10->subInt64($carry10->shiftLeft(21));
        $carry12 = $s12->addInt(1 << 20)->shiftRight(21);
        $s13 = $s13->addInt64($carry12);
        $s12 = $s12->subInt64($carry12->shiftLeft(21));
        $carry14 = $s14->addInt(1 << 20)->shiftRight(21);
        $s15 = $s15->addInt64($carry14);
        $s14 = $s14->subInt64($carry14->shiftLeft(21));
        $carry16 = $s16->addInt(1 << 20)->shiftRight(21);
        $s17 = $s17->addInt64($carry16);
        $s16 = $s16->subInt64($carry16->shiftLeft(21));

        $carry7 = $s7->addInt(1 << 20)->shiftRight(21);
        $s8 = $s8->addInt64($carry7);
        $s7 = $s7->subInt64($carry7->shiftLeft(21));
        $carry9 = $s9->addInt(1 << 20)->shiftRight(21);
        $s10 = $s10->addInt64($carry9);
        $s9 = $s9->subInt64($carry9->shiftLeft(21));
        $carry11 = $s11->addInt(1 << 20)->shiftRight(21);
        $s12 = $s12->addInt64($carry11);
        $s11 = $s11->subInt64($carry11->shiftLeft(21));
        $carry13 = $s13->addInt(1 << 20)->shiftRight(21);
        $s14 = $s14->addInt64($carry13);
        $s13 = $s13->subInt64($carry13->shiftLeft(21));
        $carry15 = $s15->addInt(1 << 20)->shiftRight(21);
        $s16 = $s16->addInt64($carry15);
        $s15 = $s15->subInt64($carry15->shiftLeft(21));

        $s5  =  $s5->addInt64($s17->mulInt(666643, 20));
        $s6  =  $s6->addInt64($s17->mulInt(470296, 19));
        $s7  =  $s7->addInt64($s17->mulInt(654183, 20));
        $s8  =  $s8->subInt64($s17->mulInt(997805, 20));
        $s9  =  $s9->addInt64($s17->mulInt(136657, 18));
        $s10 = $s10->subInt64($s17->mulInt(683901, 20));

        $s4  =  $s4->addInt64($s16->mulInt(666643, 20));
        $s5  =  $s5->addInt64($s16->mulInt(470296, 19));
        $s6  =  $s6->addInt64($s16->mulInt(654183, 20));
        $s7  =  $s7->subInt64($s16->mulInt(997805, 20));
        $s8  =  $s8->addInt64($s16->mulInt(136657, 18));
        $s9  =  $s9->subInt64($s16->mulInt(683901, 20));

        $s3  =  $s3->addInt64($s15->mulInt(666643, 20));
        $s4  =  $s4->addInt64($s15->mulInt(470296, 19));
        $s5  =  $s5->addInt64($s15->mulInt(654183, 20));
        $s6  =  $s6->subInt64($s15->mulInt(997805, 20));
        $s7  =  $s7->addInt64($s15->mulInt(136657, 18));
        $s8  =  $s8->subInt64($s15->mulInt(683901, 20));

        $s2  =  $s2->addInt64($s14->mulInt(666643, 20));
        $s3  =  $s3->addInt64($s14->mulInt(470296, 19));
        $s4  =  $s4->addInt64($s14->mulInt(654183, 20));
        $s5  =  $s5->subInt64($s14->mulInt(997805, 20));
        $s6  =  $s6->addInt64($s14->mulInt(136657, 18));
        $s7  =  $s7->subInt64($s14->mulInt(683901, 20));

        $s1  =  $s1->addInt64($s13->mulInt(666643, 20));
        $s2  =  $s2->addInt64($s13->mulInt(470296, 19));
        $s3  =  $s3->addInt64($s13->mulInt(654183, 20));
        $s4  =  $s4->subInt64($s13->mulInt(997805, 20));
        $s5  =  $s5->addInt64($s13->mulInt(136657, 18));
        $s6  =  $s6->subInt64($s13->mulInt(683901, 20));

        $s0  =  $s0->addInt64($s12->mulInt(666643, 20));
        $s1  =  $s1->addInt64($s12->mulInt(470296, 19));
        $s2  =  $s2->addInt64($s12->mulInt(654183, 20));
        $s3  =  $s3->subInt64($s12->mulInt(997805, 20));
        $s4  =  $s4->addInt64($s12->mulInt(136657, 18));
        $s5  =  $s5->subInt64($s12->mulInt(683901, 20));
        $s12 = new ParagonIE_Sodium_Core32_Int64();

        $carry0 = $s0->addInt(1 << 20)->shiftRight(21);
        $s1 = $s1->addInt64($carry0);
        $s0 = $s0->subInt64($carry0->shiftLeft(21));
        $carry2 = $s2->addInt(1 << 20)->shiftRight(21);
        $s3 = $s3->addInt64($carry2);
        $s2 = $s2->subInt64($carry2->shiftLeft(21));
        $carry4 = $s4->addInt(1 << 20)->shiftRight(21);
        $s5 = $s5->addInt64($carry4);
        $s4 = $s4->subInt64($carry4->shiftLeft(21));
        $carry6 = $s6->addInt(1 << 20)->shiftRight(21);
        $s7 = $s7->addInt64($carry6);
        $s6 = $s6->subInt64($carry6->shiftLeft(21));
        $carry8 = $s8->addInt(1 << 20)->shiftRight(21);
        $s9 = $s9->addInt64($carry8);
        $s8 = $s8->subInt64($carry8->shiftLeft(21));
        $carry10 = $s10->addInt(1 << 20)->shiftRight(21);
        $s11 = $s11->addInt64($carry10);
        $s10 = $s10->subInt64($carry10->shiftLeft(21));
        $carry1 = $s1->addInt(1 << 20)->shiftRight(21);
        $s2 = $s2->addInt64($carry1);
        $s1 = $s1->subInt64($carry1->shiftLeft(21));
        $carry3 = $s3->addInt(1 << 20)->shiftRight(21);
        $s4 = $s4->addInt64($carry3);
        $s3 = $s3->subInt64($carry3->shiftLeft(21));
        $carry5 = $s5->addInt(1 << 20)->shiftRight(21);
        $s6 = $s6->addInt64($carry5);
        $s5 = $s5->subInt64($carry5->shiftLeft(21));
        $carry7 = $s7->addInt(1 << 20)->shiftRight(21);
        $s8 = $s8->addInt64($carry7);
        $s7 = $s7->subInt64($carry7->shiftLeft(21));
        $carry9 = $s9->addInt(1 << 20)->shiftRight(21);
        $s10 = $s10->addInt64($carry9);
        $s9 = $s9->subInt64($carry9->shiftLeft(21));
        $carry11 = $s11->addInt(1 << 20)->shiftRight(21);
        $s12 = $s12->addInt64($carry11);
        $s11 = $s11->subInt64($carry11->shiftLeft(21));

        $s0  =  $s0->addInt64($s12->mulInt(666643, 20));
        $s1  =  $s1->addInt64($s12->mulInt(470296, 19));
        $s2  =  $s2->addInt64($s12->mulInt(654183, 20));
        $s3  =  $s3->subInt64($s12->mulInt(997805, 20));
        $s4  =  $s4->addInt64($s12->mulInt(136657, 18));
        $s5  =  $s5->subInt64($s12->mulInt(683901, 20));
        $s12 = new ParagonIE_Sodium_Core32_Int64();

        $carry0 = $s0->shiftRight(21);
        $s1 = $s1->addInt64($carry0);
        $s0 = $s0->subInt64($carry0->shiftLeft(21));
        $carry1 = $s1->shiftRight(21);
        $s2 = $s2->addInt64($carry1);
        $s1 = $s1->subInt64($carry1->shiftLeft(21));
        $carry2 = $s2->shiftRight(21);
        $s3 = $s3->addInt64($carry2);
        $s2 = $s2->subInt64($carry2->shiftLeft(21));
        $carry3 = $s3->shiftRight(21);
        $s4 = $s4->addInt64($carry3);
        $s3 = $s3->subInt64($carry3->shiftLeft(21));
        $carry4 = $s4->shiftRight(21);
        $s5 = $s5->addInt64($carry4);
        $s4 = $s4->subInt64($carry4->shiftLeft(21));
        $carry5 = $s5->shiftRight(21);
        $s6 = $s6->addInt64($carry5);
        $s5 = $s5->subInt64($carry5->shiftLeft(21));
        $carry6 = $s6->shiftRight(21);
        $s7 = $s7->addInt64($carry6);
        $s6 = $s6->subInt64($carry6->shiftLeft(21));
        $carry7 = $s7->shiftRight(21);
        $s8 = $s8->addInt64($carry7);
        $s7 = $s7->subInt64($carry7->shiftLeft(21));
        $carry8 = $s8->shiftRight(21);
        $s9 = $s9->addInt64($carry8);
        $s8 = $s8->subInt64($carry8->shiftLeft(21));
        $carry9 = $s9->shiftRight(21);
        $s10 = $s10->addInt64($carry9);
        $s9 = $s9->subInt64($carry9->shiftLeft(21));
        $carry10 = $s10->shiftRight(21);
        $s11 = $s11->addInt64($carry10);
        $s10 = $s10->subInt64($carry10->shiftLeft(21));
        $carry11 = $s11->shiftRight(21);
        $s12 = $s12->addInt64($carry11);
        $s11 = $s11->subInt64($carry11->shiftLeft(21));

        $s0  =  $s0->addInt64($s12->mulInt(666643, 20));
        $s1  =  $s1->addInt64($s12->mulInt(470296, 19));
        $s2  =  $s2->addInt64($s12->mulInt(654183, 20));
        $s3  =  $s3->subInt64($s12->mulInt(997805, 20));
        $s4  =  $s4->addInt64($s12->mulInt(136657, 18));
        $s5  =  $s5->subInt64($s12->mulInt(683901, 20));

        $carry0 = $s0->shiftRight(21);
        $s1 = $s1->addInt64($carry0);
        $s0 = $s0->subInt64($carry0->shiftLeft(21));
        $carry1 = $s1->shiftRight(21);
        $s2 = $s2->addInt64($carry1);
        $s1 = $s1->subInt64($carry1->shiftLeft(21));
        $carry2 = $s2->shiftRight(21);
        $s3 = $s3->addInt64($carry2);
        $s2 = $s2->subInt64($carry2->shiftLeft(21));
        $carry3 = $s3->shiftRight(21);
        $s4 = $s4->addInt64($carry3);
        $s3 = $s3->subInt64($carry3->shiftLeft(21));
        $carry4 = $s4->shiftRight(21);
        $s5 = $s5->addInt64($carry4);
        $s4 = $s4->subInt64($carry4->shiftLeft(21));
        $carry5 = $s5->shiftRight(21);
        $s6 = $s6->addInt64($carry5);
        $s5 = $s5->subInt64($carry5->shiftLeft(21));
        $carry6 = $s6->shiftRight(21);
        $s7 = $s7->addInt64($carry6);
        $s6 = $s6->subInt64($carry6->shiftLeft(21));
        $carry7 = $s7->shiftRight(21);
        $s8 = $s8->addInt64($carry7);
        $s7 = $s7->subInt64($carry7->shiftLeft(21));
        $carry8 = $s8->shiftRight(21);
        $s9 = $s9->addInt64($carry8);
        $s8 = $s8->subInt64($carry8->shiftLeft(21));
        $carry9 = $s9->shiftRight(21);
        $s10 = $s10->addInt64($carry9);
        $s9 = $s9->subInt64($carry9->shiftLeft(21));
        $carry10 = $s10->shiftRight(21);
        $s11 = $s11->addInt64($carry10);
        $s10 = $s10->subInt64($carry10->shiftLeft(21));

        $S0 = $s0->toInt32()->toInt();
        $S1 = $s1->toInt32()->toInt();
        $S2 = $s2->toInt32()->toInt();
        $S3 = $s3->toInt32()->toInt();
        $S4 = $s4->toInt32()->toInt();
        $S5 = $s5->toInt32()->toInt();
        $S6 = $s6->toInt32()->toInt();
        $S7 = $s7->toInt32()->toInt();
        $S8 = $s8->toInt32()->toInt();
        $S9 = $s9->toInt32()->toInt();
        $S10 = $s10->toInt32()->toInt();
        $S11 = $s11->toInt32()->toInt();

        /**
         * @var array<int, int>
         */
        $arr = array(
            (int) ($S0 >> 0),
            (int) ($S0 >> 8),
            (int) (($S0 >> 16) | ($S1 << 5)),
            (int) ($S1 >> 3),
            (int) ($S1 >> 11),
            (int) (($S1 >> 19) | ($S2 << 2)),
            (int) ($S2 >> 6),
            (int) (($S2 >> 14) | ($S3 << 7)),
            (int) ($S3 >> 1),
            (int) ($S3 >> 9),
            (int) (($S3 >> 17) | ($S4 << 4)),
            (int) ($S4 >> 4),
            (int) ($S4 >> 12),
            (int) (($S4 >> 20) | ($S5 << 1)),
            (int) ($S5 >> 7),
            (int) (($S5 >> 15) | ($S6 << 6)),
            (int) ($S6 >> 2),
            (int) ($S6 >> 10),
            (int) (($S6 >> 18) | ($S7 << 3)),
            (int) ($S7 >> 5),
            (int) ($S7 >> 13),
            (int) ($S8 >> 0),
            (int) ($S8 >> 8),
            (int) (($S8 >> 16) | ($S9 << 5)),
            (int) ($S9 >> 3),
            (int) ($S9 >> 11),
            (int) (($S9 >> 19) | ($S10 << 2)),
            (int) ($S10 >> 6),
            (int) (($S10 >> 14) | ($S11 << 7)),
            (int) ($S11 >> 1),
            (int) ($S11 >> 9),
            (int) $S11 >> 17
        );
        return self::intArrayToString($arr);
    }

    /**
     * multiply by the order of the main subgroup l = 2^252+27742317777372353535851937790883648493
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $A
     * @return ParagonIE_Sodium_Core32_Curve25519_Ge_P3
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ge_mul_l(ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $A)
    {
        /** @var array<int, int> $aslide */
        $aslide = array(
            13, 0, 0, 0, 0, -1, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0,
            0, 0, 0, -3, 0, 0, 0, 0, -13, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 3, 0,
            0, 0, 0, -13, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0,
            0, 0, 11, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -1,
            0, 0, 0, 0, 3, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0,
            0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 7, 0, 0, 0, 0, 5, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
        );

        /** @var array<int, ParagonIE_Sodium_Core32_Curve25519_Ge_Cached> $Ai size 8 */
        $Ai = array();

        # ge_p3_to_cached(&Ai[0], A);
        $Ai[0] = self::ge_p3_to_cached($A);
        # ge_p3_dbl(&t, A);
        $t = self::ge_p3_dbl($A);
        # ge_p1p1_to_p3(&A2, &t);
        $A2 = self::ge_p1p1_to_p3($t);

        for ($i = 1; $i < 8; ++$i) {
            # ge_add(&t, &A2, &Ai[0]);
            $t = self::ge_add($A2, $Ai[$i - 1]);
            # ge_p1p1_to_p3(&u, &t);
            $u = self::ge_p1p1_to_p3($t);
            # ge_p3_to_cached(&Ai[i], &u);
            $Ai[$i] = self::ge_p3_to_cached($u);
        }

        $r = self::ge_p3_0();
        for ($i = 252; $i >= 0; --$i) {
            $t = self::ge_p3_dbl($r);
            if ($aslide[$i] > 0) {
                # ge_p1p1_to_p3(&u, &t);
                $u = self::ge_p1p1_to_p3($t);
                # ge_add(&t, &u, &Ai[aslide[i] / 2]);
                $t = self::ge_add($u, $Ai[(int)($aslide[$i] / 2)]);
            } elseif ($aslide[$i] < 0) {
                # ge_p1p1_to_p3(&u, &t);
                $u = self::ge_p1p1_to_p3($t);
                # ge_sub(&t, &u, &Ai[(-aslide[i]) / 2]);
                $t = self::ge_sub($u, $Ai[(int)(-$aslide[$i] / 2)]);
            }
        }
        # ge_p1p1_to_p3(r, &t);
        return self::ge_p1p1_to_p3($t);
    }
}
Core32/X25519.php000064400000025442152331075510007140 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_X25519', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_X25519
 */
abstract class ParagonIE_Sodium_Core32_X25519 extends ParagonIE_Sodium_Core32_Curve25519
{
    /**
     * Alters the objects passed to this method in place.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $f
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $g
     * @param int $b
     * @return void
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedMethodCall
     */
    public static function fe_cswap(
        ParagonIE_Sodium_Core32_Curve25519_Fe $f,
        ParagonIE_Sodium_Core32_Curve25519_Fe $g,
        $b = 0
    ) {
        $f0 = (int) $f[0]->toInt();
        $f1 = (int) $f[1]->toInt();
        $f2 = (int) $f[2]->toInt();
        $f3 = (int) $f[3]->toInt();
        $f4 = (int) $f[4]->toInt();
        $f5 = (int) $f[5]->toInt();
        $f6 = (int) $f[6]->toInt();
        $f7 = (int) $f[7]->toInt();
        $f8 = (int) $f[8]->toInt();
        $f9 = (int) $f[9]->toInt();
        $g0 = (int) $g[0]->toInt();
        $g1 = (int) $g[1]->toInt();
        $g2 = (int) $g[2]->toInt();
        $g3 = (int) $g[3]->toInt();
        $g4 = (int) $g[4]->toInt();
        $g5 = (int) $g[5]->toInt();
        $g6 = (int) $g[6]->toInt();
        $g7 = (int) $g[7]->toInt();
        $g8 = (int) $g[8]->toInt();
        $g9 = (int) $g[9]->toInt();
        $b = -$b;
        /** @var int $x0 */
        $x0 = ($f0 ^ $g0) & $b;
        /** @var int $x1 */
        $x1 = ($f1 ^ $g1) & $b;
        /** @var int $x2 */
        $x2 = ($f2 ^ $g2) & $b;
        /** @var int $x3 */
        $x3 = ($f3 ^ $g3) & $b;
        /** @var int $x4 */
        $x4 = ($f4 ^ $g4) & $b;
        /** @var int $x5 */
        $x5 = ($f5 ^ $g5) & $b;
        /** @var int $x6 */
        $x6 = ($f6 ^ $g6) & $b;
        /** @var int $x7 */
        $x7 = ($f7 ^ $g7) & $b;
        /** @var int $x8 */
        $x8 = ($f8 ^ $g8) & $b;
        /** @var int $x9 */
        $x9 = ($f9 ^ $g9) & $b;
        $f[0] = ParagonIE_Sodium_Core32_Int32::fromInt($f0 ^ $x0);
        $f[1] = ParagonIE_Sodium_Core32_Int32::fromInt($f1 ^ $x1);
        $f[2] = ParagonIE_Sodium_Core32_Int32::fromInt($f2 ^ $x2);
        $f[3] = ParagonIE_Sodium_Core32_Int32::fromInt($f3 ^ $x3);
        $f[4] = ParagonIE_Sodium_Core32_Int32::fromInt($f4 ^ $x4);
        $f[5] = ParagonIE_Sodium_Core32_Int32::fromInt($f5 ^ $x5);
        $f[6] = ParagonIE_Sodium_Core32_Int32::fromInt($f6 ^ $x6);
        $f[7] = ParagonIE_Sodium_Core32_Int32::fromInt($f7 ^ $x7);
        $f[8] = ParagonIE_Sodium_Core32_Int32::fromInt($f8 ^ $x8);
        $f[9] = ParagonIE_Sodium_Core32_Int32::fromInt($f9 ^ $x9);
        $g[0] = ParagonIE_Sodium_Core32_Int32::fromInt($g0 ^ $x0);
        $g[1] = ParagonIE_Sodium_Core32_Int32::fromInt($g1 ^ $x1);
        $g[2] = ParagonIE_Sodium_Core32_Int32::fromInt($g2 ^ $x2);
        $g[3] = ParagonIE_Sodium_Core32_Int32::fromInt($g3 ^ $x3);
        $g[4] = ParagonIE_Sodium_Core32_Int32::fromInt($g4 ^ $x4);
        $g[5] = ParagonIE_Sodium_Core32_Int32::fromInt($g5 ^ $x5);
        $g[6] = ParagonIE_Sodium_Core32_Int32::fromInt($g6 ^ $x6);
        $g[7] = ParagonIE_Sodium_Core32_Int32::fromInt($g7 ^ $x7);
        $g[8] = ParagonIE_Sodium_Core32_Int32::fromInt($g8 ^ $x8);
        $g[9] = ParagonIE_Sodium_Core32_Int32::fromInt($g9 ^ $x9);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $f
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedMethodCall
     */
    public static function fe_mul121666(ParagonIE_Sodium_Core32_Curve25519_Fe $f)
    {
        /** @var array<int, ParagonIE_Sodium_Core32_Int64> $h */
        $h = array();
        for ($i = 0; $i < 10; ++$i) {
            $h[$i] = $f[$i]->toInt64()->mulInt(121666, 17);
        }

        $carry9 = $h[9]->addInt(1 << 24)->shiftRight(25);
        $h[0] = $h[0]->addInt64($carry9->mulInt(19, 5));
        $h[9] = $h[9]->subInt64($carry9->shiftLeft(25));

        $carry1 = $h[1]->addInt(1 << 24)->shiftRight(25);
        $h[2] = $h[2]->addInt64($carry1);
        $h[1] = $h[1]->subInt64($carry1->shiftLeft(25));

        $carry3 = $h[3]->addInt(1 << 24)->shiftRight(25);
        $h[4] = $h[4]->addInt64($carry3);
        $h[3] = $h[3]->subInt64($carry3->shiftLeft(25));

        $carry5 = $h[5]->addInt(1 << 24)->shiftRight(25);
        $h[6] = $h[6]->addInt64($carry5);
        $h[5] = $h[5]->subInt64($carry5->shiftLeft(25));

        $carry7 = $h[7]->addInt(1 << 24)->shiftRight(25);
        $h[8] = $h[8]->addInt64($carry7);
        $h[7] = $h[7]->subInt64($carry7->shiftLeft(25));

        $carry0 = $h[0]->addInt(1 << 25)->shiftRight(26);
        $h[1] = $h[1]->addInt64($carry0);
        $h[0] = $h[0]->subInt64($carry0->shiftLeft(26));

        $carry2 = $h[2]->addInt(1 << 25)->shiftRight(26);
        $h[3] = $h[3]->addInt64($carry2);
        $h[2] = $h[2]->subInt64($carry2->shiftLeft(26));

        $carry4 = $h[4]->addInt(1 << 25)->shiftRight(26);
        $h[5] = $h[5]->addInt64($carry4);
        $h[4] = $h[4]->subInt64($carry4->shiftLeft(26));

        $carry6 = $h[6]->addInt(1 << 25)->shiftRight(26);
        $h[7] = $h[7]->addInt64($carry6);
        $h[6] = $h[6]->subInt64($carry6->shiftLeft(26));

        $carry8 = $h[8]->addInt(1 << 25)->shiftRight(26);
        $h[9] = $h[9]->addInt64($carry8);
        $h[8] = $h[8]->subInt64($carry8->shiftLeft(26));

        for ($i = 0; $i < 10; ++$i) {
            $h[$i] = $h[$i]->toInt32();
        }
        /** @var array<int, ParagonIE_Sodium_Core32_Int32> $h2 */
        $h2 = $h;
        return ParagonIE_Sodium_Core32_Curve25519_Fe::fromArray($h2);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * Inline comments preceded by # are from libsodium's ref10 code.
     *
     * @param string $n
     * @param string $p
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function crypto_scalarmult_curve25519_ref10($n, $p)
    {
        # for (i = 0;i < 32;++i) e[i] = n[i];
        $e = '' . $n;
        # e[0] &= 248;
        $e[0] = self::intToChr(
            self::chrToInt($e[0]) & 248
        );
        # e[31] &= 127;
        # e[31] |= 64;
        $e[31] = self::intToChr(
            (self::chrToInt($e[31]) & 127) | 64
        );
        # fe_frombytes(x1,p);
        $x1 = self::fe_frombytes($p);
        # fe_1(x2);
        $x2 = self::fe_1();
        # fe_0(z2);
        $z2 = self::fe_0();
        # fe_copy(x3,x1);
        $x3 = self::fe_copy($x1);
        # fe_1(z3);
        $z3 = self::fe_1();

        # swap = 0;
        /** @var int $swap */
        $swap = 0;

        # for (pos = 254;pos >= 0;--pos) {
        for ($pos = 254; $pos >= 0; --$pos) {
            # b = e[pos / 8] >> (pos & 7);
            /** @var int $b */
            $b = self::chrToInt(
                    $e[(int) floor($pos / 8)]
                ) >> ($pos & 7);
            # b &= 1;
            $b &= 1;

            # swap ^= b;
            $swap ^= $b;

            # fe_cswap(x2,x3,swap);
            self::fe_cswap($x2, $x3, $swap);

            # fe_cswap(z2,z3,swap);
            self::fe_cswap($z2, $z3, $swap);

            # swap = b;
            /** @var int $swap */
            $swap = $b;

            # fe_sub(tmp0,x3,z3);
            $tmp0 = self::fe_sub($x3, $z3);

            # fe_sub(tmp1,x2,z2);
            $tmp1 = self::fe_sub($x2, $z2);

            # fe_add(x2,x2,z2);
            $x2 = self::fe_add($x2, $z2);

            # fe_add(z2,x3,z3);
            $z2 = self::fe_add($x3, $z3);

            # fe_mul(z3,tmp0,x2);
            $z3 = self::fe_mul($tmp0, $x2);

            # fe_mul(z2,z2,tmp1);
            $z2 = self::fe_mul($z2, $tmp1);

            # fe_sq(tmp0,tmp1);
            $tmp0 = self::fe_sq($tmp1);

            # fe_sq(tmp1,x2);
            $tmp1 = self::fe_sq($x2);

            # fe_add(x3,z3,z2);
            $x3 = self::fe_add($z3, $z2);

            # fe_sub(z2,z3,z2);
            $z2 = self::fe_sub($z3, $z2);

            # fe_mul(x2,tmp1,tmp0);
            $x2 = self::fe_mul($tmp1, $tmp0);

            # fe_sub(tmp1,tmp1,tmp0);
            $tmp1 = self::fe_sub($tmp1, $tmp0);

            # fe_sq(z2,z2);
            $z2 = self::fe_sq($z2);

            # fe_mul121666(z3,tmp1);
            $z3 = self::fe_mul121666($tmp1);

            # fe_sq(x3,x3);
            $x3 = self::fe_sq($x3);

            # fe_add(tmp0,tmp0,z3);
            $tmp0 = self::fe_add($tmp0, $z3);

            # fe_mul(z3,x1,z2);
            $z3 = self::fe_mul($x1, $z2);

            # fe_mul(z2,tmp1,tmp0);
            $z2 = self::fe_mul($tmp1, $tmp0);
        }

        # fe_cswap(x2,x3,swap);
        self::fe_cswap($x2, $x3, $swap);

        # fe_cswap(z2,z3,swap);
        self::fe_cswap($z2, $z3, $swap);

        # fe_invert(z2,z2);
        $z2 = self::fe_invert($z2);

        # fe_mul(x2,x2,z2);
        $x2 = self::fe_mul($x2, $z2);
        # fe_tobytes(q,x2);
        return (string) self::fe_tobytes($x2);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $edwardsY
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $edwardsZ
     * @return ParagonIE_Sodium_Core32_Curve25519_Fe
     * @throws SodiumException
     * @throws TypeError
     */
    public static function edwards_to_montgomery(
        ParagonIE_Sodium_Core32_Curve25519_Fe $edwardsY,
        ParagonIE_Sodium_Core32_Curve25519_Fe $edwardsZ
    ) {
        $tempX = self::fe_add($edwardsZ, $edwardsY);
        $tempZ = self::fe_sub($edwardsZ, $edwardsY);
        $tempZ = self::fe_invert($tempZ);
        return self::fe_mul($tempX, $tempZ);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $n
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function crypto_scalarmult_curve25519_ref10_base($n)
    {
        # for (i = 0;i < 32;++i) e[i] = n[i];
        $e = '' . $n;

        # e[0] &= 248;
        $e[0] = self::intToChr(
            self::chrToInt($e[0]) & 248
        );

        # e[31] &= 127;
        # e[31] |= 64;
        $e[31] = self::intToChr(
            (self::chrToInt($e[31]) & 127) | 64
        );

        $A = self::ge_scalarmult_base($e);
        if (
            !($A->Y instanceof ParagonIE_Sodium_Core32_Curve25519_Fe)
                ||
            !($A->Z instanceof ParagonIE_Sodium_Core32_Curve25519_Fe)
        ) {
            throw new TypeError('Null points encountered');
        }
        $pk = self::edwards_to_montgomery($A->Y, $A->Z);
        return self::fe_tobytes($pk);
    }
}
Core32/HSalsa20.php000064400000015435152331075510007641 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_HSalsa20', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_HSalsa20
 */
abstract class ParagonIE_Sodium_Core32_HSalsa20 extends ParagonIE_Sodium_Core32_Salsa20
{
    /**
     * Calculate an hsalsa20 hash of a single block
     *
     * HSalsa20 doesn't have a counter and will never be used for more than
     * one block (used to derive a subkey for xsalsa20).
     *
     * @internal You should not use this directly from another application
     *
     * @param string $in
     * @param string $k
     * @param string|null $c
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function hsalsa20($in, $k, $c = null)
    {
        /**
         * @var ParagonIE_Sodium_Core32_Int32 $x0
         * @var ParagonIE_Sodium_Core32_Int32 $x1
         * @var ParagonIE_Sodium_Core32_Int32 $x2
         * @var ParagonIE_Sodium_Core32_Int32 $x3
         * @var ParagonIE_Sodium_Core32_Int32 $x4
         * @var ParagonIE_Sodium_Core32_Int32 $x5
         * @var ParagonIE_Sodium_Core32_Int32 $x6
         * @var ParagonIE_Sodium_Core32_Int32 $x7
         * @var ParagonIE_Sodium_Core32_Int32 $x8
         * @var ParagonIE_Sodium_Core32_Int32 $x9
         * @var ParagonIE_Sodium_Core32_Int32 $x10
         * @var ParagonIE_Sodium_Core32_Int32 $x11
         * @var ParagonIE_Sodium_Core32_Int32 $x12
         * @var ParagonIE_Sodium_Core32_Int32 $x13
         * @var ParagonIE_Sodium_Core32_Int32 $x14
         * @var ParagonIE_Sodium_Core32_Int32 $x15
         * @var ParagonIE_Sodium_Core32_Int32 $j0
         * @var ParagonIE_Sodium_Core32_Int32 $j1
         * @var ParagonIE_Sodium_Core32_Int32 $j2
         * @var ParagonIE_Sodium_Core32_Int32 $j3
         * @var ParagonIE_Sodium_Core32_Int32 $j4
         * @var ParagonIE_Sodium_Core32_Int32 $j5
         * @var ParagonIE_Sodium_Core32_Int32 $j6
         * @var ParagonIE_Sodium_Core32_Int32 $j7
         * @var ParagonIE_Sodium_Core32_Int32 $j8
         * @var ParagonIE_Sodium_Core32_Int32 $j9
         * @var ParagonIE_Sodium_Core32_Int32 $j10
         * @var ParagonIE_Sodium_Core32_Int32 $j11
         * @var ParagonIE_Sodium_Core32_Int32 $j12
         * @var ParagonIE_Sodium_Core32_Int32 $j13
         * @var ParagonIE_Sodium_Core32_Int32 $j14
         * @var ParagonIE_Sodium_Core32_Int32 $j15
         */
        if (self::strlen($k) < 32) {
            throw new RangeException('Key must be 32 bytes long');
        }
        if ($c === null) {
            $x0  = new ParagonIE_Sodium_Core32_Int32(array(0x6170, 0x7865));
            $x5  = new ParagonIE_Sodium_Core32_Int32(array(0x3320, 0x646e));
            $x10 = new ParagonIE_Sodium_Core32_Int32(array(0x7962, 0x2d32));
            $x15 = new ParagonIE_Sodium_Core32_Int32(array(0x6b20, 0x6574));
        } else {
            $x0  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($c, 0, 4));
            $x5  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($c, 4, 4));
            $x10 = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($c, 8, 4));
            $x15 = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($c, 12, 4));
        }
        $x1  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 0, 4));
        $x2  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 4, 4));
        $x3  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 8, 4));
        $x4  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 12, 4));
        $x6  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($in, 0, 4));
        $x7  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($in, 4, 4));
        $x8  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($in, 8, 4));
        $x9  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($in, 12, 4));
        $x11 = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 16, 4));
        $x12 = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 20, 4));
        $x13 = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 24, 4));
        $x14 = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 28, 4));

        for ($i = self::ROUNDS; $i > 0; $i -= 2) {
            $x4  = $x4->xorInt32($x0->addInt32($x12)->rotateLeft(7));
            $x8  = $x8->xorInt32($x4->addInt32($x0)->rotateLeft(9));
            $x12 = $x12->xorInt32($x8->addInt32($x4)->rotateLeft(13));
            $x0  = $x0->xorInt32($x12->addInt32($x8)->rotateLeft(18));

            $x9  = $x9->xorInt32($x5->addInt32($x1)->rotateLeft(7));
            $x13 = $x13->xorInt32($x9->addInt32($x5)->rotateLeft(9));
            $x1  = $x1->xorInt32($x13->addInt32($x9)->rotateLeft(13));
            $x5  = $x5->xorInt32($x1->addInt32($x13)->rotateLeft(18));

            $x14 = $x14->xorInt32($x10->addInt32($x6)->rotateLeft(7));
            $x2  = $x2->xorInt32($x14->addInt32($x10)->rotateLeft(9));
            $x6  = $x6->xorInt32($x2->addInt32($x14)->rotateLeft(13));
            $x10 = $x10->xorInt32($x6->addInt32($x2)->rotateLeft(18));

            $x3  = $x3->xorInt32($x15->addInt32($x11)->rotateLeft(7));
            $x7  = $x7->xorInt32($x3->addInt32($x15)->rotateLeft(9));
            $x11 = $x11->xorInt32($x7->addInt32($x3)->rotateLeft(13));
            $x15 = $x15->xorInt32($x11->addInt32($x7)->rotateLeft(18));

            $x1  = $x1->xorInt32($x0->addInt32($x3)->rotateLeft(7));
            $x2  = $x2->xorInt32($x1->addInt32($x0)->rotateLeft(9));
            $x3  = $x3->xorInt32($x2->addInt32($x1)->rotateLeft(13));
            $x0  = $x0->xorInt32($x3->addInt32($x2)->rotateLeft(18));

            $x6  = $x6->xorInt32($x5->addInt32($x4)->rotateLeft(7));
            $x7  = $x7->xorInt32($x6->addInt32($x5)->rotateLeft(9));
            $x4  = $x4->xorInt32($x7->addInt32($x6)->rotateLeft(13));
            $x5  = $x5->xorInt32($x4->addInt32($x7)->rotateLeft(18));

            $x11 = $x11->xorInt32($x10->addInt32($x9)->rotateLeft(7));
            $x8  = $x8->xorInt32($x11->addInt32($x10)->rotateLeft(9));
            $x9  = $x9->xorInt32($x8->addInt32($x11)->rotateLeft(13));
            $x10 = $x10->xorInt32($x9->addInt32($x8)->rotateLeft(18));

            $x12 = $x12->xorInt32($x15->addInt32($x14)->rotateLeft(7));
            $x13 = $x13->xorInt32($x12->addInt32($x15)->rotateLeft(9));
            $x14 = $x14->xorInt32($x13->addInt32($x12)->rotateLeft(13));
            $x15 = $x15->xorInt32($x14->addInt32($x13)->rotateLeft(18));
        }

        return $x0->toReverseString() .
            $x5->toReverseString() .
            $x10->toReverseString() .
            $x15->toReverseString() .
            $x6->toReverseString() .
            $x7->toReverseString() .
            $x8->toReverseString() .
            $x9->toReverseString();
    }
}
Core32/ChaCha20.php000064400000034257152331075510007600 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_ChaCha20', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_ChaCha20
 */
class ParagonIE_Sodium_Core32_ChaCha20 extends ParagonIE_Sodium_Core32_Util
{
    /**
     * The ChaCha20 quarter round function. Works on four 32-bit integers.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Int32 $a
     * @param ParagonIE_Sodium_Core32_Int32 $b
     * @param ParagonIE_Sodium_Core32_Int32 $c
     * @param ParagonIE_Sodium_Core32_Int32 $d
     * @return array<int, ParagonIE_Sodium_Core32_Int32>
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function quarterRound(
        ParagonIE_Sodium_Core32_Int32 $a,
        ParagonIE_Sodium_Core32_Int32 $b,
        ParagonIE_Sodium_Core32_Int32 $c,
        ParagonIE_Sodium_Core32_Int32 $d
    ) {
        /** @var ParagonIE_Sodium_Core32_Int32 $a */
        /** @var ParagonIE_Sodium_Core32_Int32 $b */
        /** @var ParagonIE_Sodium_Core32_Int32 $c */
        /** @var ParagonIE_Sodium_Core32_Int32 $d */

        # a = PLUS(a,b); d = ROTATE(XOR(d,a),16);
        $a = $a->addInt32($b);
        $d = $d->xorInt32($a)->rotateLeft(16);

        # c = PLUS(c,d); b = ROTATE(XOR(b,c),12);
        $c = $c->addInt32($d);
        $b = $b->xorInt32($c)->rotateLeft(12);

        # a = PLUS(a,b); d = ROTATE(XOR(d,a), 8);
        $a = $a->addInt32($b);
        $d = $d->xorInt32($a)->rotateLeft(8);

        # c = PLUS(c,d); b = ROTATE(XOR(b,c), 7);
        $c = $c->addInt32($d);
        $b = $b->xorInt32($c)->rotateLeft(7);

        return array($a, $b, $c, $d);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_ChaCha20_Ctx $ctx
     * @param string $message
     *
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function encryptBytes(
        ParagonIE_Sodium_Core32_ChaCha20_Ctx $ctx,
        $message = ''
    ) {
        $bytes = self::strlen($message);

        /** @var ParagonIE_Sodium_Core32_Int32 $x0 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x1 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x2 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x3 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x4 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x5 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x6 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x7 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x8 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x9 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x10 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x11 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x12 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x13 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x14 */
        /** @var ParagonIE_Sodium_Core32_Int32 $x15 */

        /*
        j0 = ctx->input[0];
        j1 = ctx->input[1];
        j2 = ctx->input[2];
        j3 = ctx->input[3];
        j4 = ctx->input[4];
        j5 = ctx->input[5];
        j6 = ctx->input[6];
        j7 = ctx->input[7];
        j8 = ctx->input[8];
        j9 = ctx->input[9];
        j10 = ctx->input[10];
        j11 = ctx->input[11];
        j12 = ctx->input[12];
        j13 = ctx->input[13];
        j14 = ctx->input[14];
        j15 = ctx->input[15];
        */
        /** @var ParagonIE_Sodium_Core32_Int32 $j0 */
        $j0  = $ctx[0];
        /** @var ParagonIE_Sodium_Core32_Int32 $j1 */
        $j1  = $ctx[1];
        /** @var ParagonIE_Sodium_Core32_Int32 $j2 */
        $j2  = $ctx[2];
        /** @var ParagonIE_Sodium_Core32_Int32 $j3 */
        $j3  = $ctx[3];
        /** @var ParagonIE_Sodium_Core32_Int32 $j4 */
        $j4  = $ctx[4];
        /** @var ParagonIE_Sodium_Core32_Int32 $j5 */
        $j5  = $ctx[5];
        /** @var ParagonIE_Sodium_Core32_Int32 $j6 */
        $j6  = $ctx[6];
        /** @var ParagonIE_Sodium_Core32_Int32 $j7 */
        $j7  = $ctx[7];
        /** @var ParagonIE_Sodium_Core32_Int32 $j8 */
        $j8  = $ctx[8];
        /** @var ParagonIE_Sodium_Core32_Int32 $j9 */
        $j9  = $ctx[9];
        /** @var ParagonIE_Sodium_Core32_Int32 $j10 */
        $j10 = $ctx[10];
        /** @var ParagonIE_Sodium_Core32_Int32 $j11 */
        $j11 = $ctx[11];
        /** @var ParagonIE_Sodium_Core32_Int32 $j12 */
        $j12 = $ctx[12];
        /** @var ParagonIE_Sodium_Core32_Int32 $j13 */
        $j13 = $ctx[13];
        /** @var ParagonIE_Sodium_Core32_Int32 $j14 */
        $j14 = $ctx[14];
        /** @var ParagonIE_Sodium_Core32_Int32 $j15 */
        $j15 = $ctx[15];

        $c = '';
        for (;;) {
            if ($bytes < 64) {
                $message .= str_repeat("\x00", 64 - $bytes);
            }

            $x0 =  clone $j0;
            $x1 =  clone $j1;
            $x2 =  clone $j2;
            $x3 =  clone $j3;
            $x4 =  clone $j4;
            $x5 =  clone $j5;
            $x6 =  clone $j6;
            $x7 =  clone $j7;
            $x8 =  clone $j8;
            $x9 =  clone $j9;
            $x10 = clone $j10;
            $x11 = clone $j11;
            $x12 = clone $j12;
            $x13 = clone $j13;
            $x14 = clone $j14;
            $x15 = clone $j15;

            # for (i = 20; i > 0; i -= 2) {
            for ($i = 20; $i > 0; $i -= 2) {
                # QUARTERROUND( x0,  x4,  x8,  x12)
                list($x0, $x4, $x8, $x12) = self::quarterRound($x0, $x4, $x8, $x12);

                # QUARTERROUND( x1,  x5,  x9,  x13)
                list($x1, $x5, $x9, $x13) = self::quarterRound($x1, $x5, $x9, $x13);

                # QUARTERROUND( x2,  x6,  x10,  x14)
                list($x2, $x6, $x10, $x14) = self::quarterRound($x2, $x6, $x10, $x14);

                # QUARTERROUND( x3,  x7,  x11,  x15)
                list($x3, $x7, $x11, $x15) = self::quarterRound($x3, $x7, $x11, $x15);

                # QUARTERROUND( x0,  x5,  x10,  x15)
                list($x0, $x5, $x10, $x15) = self::quarterRound($x0, $x5, $x10, $x15);

                # QUARTERROUND( x1,  x6,  x11,  x12)
                list($x1, $x6, $x11, $x12) = self::quarterRound($x1, $x6, $x11, $x12);

                # QUARTERROUND( x2,  x7,  x8,  x13)
                list($x2, $x7, $x8, $x13) = self::quarterRound($x2, $x7, $x8, $x13);

                # QUARTERROUND( x3,  x4,  x9,  x14)
                list($x3, $x4, $x9, $x14) = self::quarterRound($x3, $x4, $x9, $x14);
            }
            /*
            x0 = PLUS(x0, j0);
            x1 = PLUS(x1, j1);
            x2 = PLUS(x2, j2);
            x3 = PLUS(x3, j3);
            x4 = PLUS(x4, j4);
            x5 = PLUS(x5, j5);
            x6 = PLUS(x6, j6);
            x7 = PLUS(x7, j7);
            x8 = PLUS(x8, j8);
            x9 = PLUS(x9, j9);
            x10 = PLUS(x10, j10);
            x11 = PLUS(x11, j11);
            x12 = PLUS(x12, j12);
            x13 = PLUS(x13, j13);
            x14 = PLUS(x14, j14);
            x15 = PLUS(x15, j15);
            */
            $x0 = $x0->addInt32($j0);
            $x1 = $x1->addInt32($j1);
            $x2 = $x2->addInt32($j2);
            $x3 = $x3->addInt32($j3);
            $x4 = $x4->addInt32($j4);
            $x5 = $x5->addInt32($j5);
            $x6 = $x6->addInt32($j6);
            $x7 = $x7->addInt32($j7);
            $x8 = $x8->addInt32($j8);
            $x9 = $x9->addInt32($j9);
            $x10 = $x10->addInt32($j10);
            $x11 = $x11->addInt32($j11);
            $x12 = $x12->addInt32($j12);
            $x13 = $x13->addInt32($j13);
            $x14 = $x14->addInt32($j14);
            $x15 = $x15->addInt32($j15);

            /*
            x0 = XOR(x0, LOAD32_LE(m + 0));
            x1 = XOR(x1, LOAD32_LE(m + 4));
            x2 = XOR(x2, LOAD32_LE(m + 8));
            x3 = XOR(x3, LOAD32_LE(m + 12));
            x4 = XOR(x4, LOAD32_LE(m + 16));
            x5 = XOR(x5, LOAD32_LE(m + 20));
            x6 = XOR(x6, LOAD32_LE(m + 24));
            x7 = XOR(x7, LOAD32_LE(m + 28));
            x8 = XOR(x8, LOAD32_LE(m + 32));
            x9 = XOR(x9, LOAD32_LE(m + 36));
            x10 = XOR(x10, LOAD32_LE(m + 40));
            x11 = XOR(x11, LOAD32_LE(m + 44));
            x12 = XOR(x12, LOAD32_LE(m + 48));
            x13 = XOR(x13, LOAD32_LE(m + 52));
            x14 = XOR(x14, LOAD32_LE(m + 56));
            x15 = XOR(x15, LOAD32_LE(m + 60));
            */
            $x0  =  $x0->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message,  0, 4)));
            $x1  =  $x1->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message,  4, 4)));
            $x2  =  $x2->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message,  8, 4)));
            $x3  =  $x3->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 12, 4)));
            $x4  =  $x4->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 16, 4)));
            $x5  =  $x5->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 20, 4)));
            $x6  =  $x6->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 24, 4)));
            $x7  =  $x7->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 28, 4)));
            $x8  =  $x8->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 32, 4)));
            $x9  =  $x9->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 36, 4)));
            $x10 = $x10->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 40, 4)));
            $x11 = $x11->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 44, 4)));
            $x12 = $x12->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 48, 4)));
            $x13 = $x13->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 52, 4)));
            $x14 = $x14->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 56, 4)));
            $x15 = $x15->xorInt32(ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 60, 4)));

            /*
                j12 = PLUSONE(j12);
                if (!j12) {
                    j13 = PLUSONE(j13);
                }
             */
            /** @var ParagonIE_Sodium_Core32_Int32 $j12 */
            $j12 = $j12->addInt(1);
            if ($j12->limbs[0] === 0 && $j12->limbs[1] === 0) {
                $j13 = $j13->addInt(1);
            }

            /*
            STORE32_LE(c + 0, x0);
            STORE32_LE(c + 4, x1);
            STORE32_LE(c + 8, x2);
            STORE32_LE(c + 12, x3);
            STORE32_LE(c + 16, x4);
            STORE32_LE(c + 20, x5);
            STORE32_LE(c + 24, x6);
            STORE32_LE(c + 28, x7);
            STORE32_LE(c + 32, x8);
            STORE32_LE(c + 36, x9);
            STORE32_LE(c + 40, x10);
            STORE32_LE(c + 44, x11);
            STORE32_LE(c + 48, x12);
            STORE32_LE(c + 52, x13);
            STORE32_LE(c + 56, x14);
            STORE32_LE(c + 60, x15);
            */

            $block = $x0->toReverseString() .
                $x1->toReverseString() .
                $x2->toReverseString() .
                $x3->toReverseString() .
                $x4->toReverseString() .
                $x5->toReverseString() .
                $x6->toReverseString() .
                $x7->toReverseString() .
                $x8->toReverseString() .
                $x9->toReverseString() .
                $x10->toReverseString() .
                $x11->toReverseString() .
                $x12->toReverseString() .
                $x13->toReverseString() .
                $x14->toReverseString() .
                $x15->toReverseString();

            /* Partial block */
            if ($bytes < 64) {
                $c .= self::substr($block, 0, $bytes);
                break;
            }

            /* Full block */
            $c .= $block;
            $bytes -= 64;
            if ($bytes <= 0) {
                break;
            }
            $message = self::substr($message, 64);
        }
        /* end for(;;) loop */

        $ctx[12] = $j12;
        $ctx[13] = $j13;
        return $c;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $len
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function stream($len = 64, $nonce = '', $key = '')
    {
        return self::encryptBytes(
            new ParagonIE_Sodium_Core32_ChaCha20_Ctx($key, $nonce),
            str_repeat("\x00", $len)
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $len
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ietfStream($len, $nonce = '', $key = '')
    {
        return self::encryptBytes(
            new ParagonIE_Sodium_Core32_ChaCha20_IetfCtx($key, $nonce),
            str_repeat("\x00", $len)
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $nonce
     * @param string $key
     * @param string $ic
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function ietfStreamXorIc($message, $nonce = '', $key = '', $ic = '')
    {
        return self::encryptBytes(
            new ParagonIE_Sodium_Core32_ChaCha20_IetfCtx($key, $nonce, $ic),
            $message
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $nonce
     * @param string $key
     * @param string $ic
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function streamXorIc($message, $nonce = '', $key = '', $ic = '')
    {
        return self::encryptBytes(
            new ParagonIE_Sodium_Core32_ChaCha20_Ctx($key, $nonce, $ic),
            $message
        );
    }
}
Core32/Poly1305/State.php000064400000037154152331075510010642 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_Poly1305_State', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_Poly1305_State
 */
class ParagonIE_Sodium_Core32_Poly1305_State extends ParagonIE_Sodium_Core32_Util
{
    /**
     * @var array<int, int>
     */
    protected $buffer = array();

    /**
     * @var bool
     */
    protected $final = false;

    /**
     * @var array<int, ParagonIE_Sodium_Core32_Int32>
     */
    public $h;

    /**
     * @var int
     */
    protected $leftover = 0;

    /**
     * @var array<int, ParagonIE_Sodium_Core32_Int32>
     */
    public $r;

    /**
     * @var array<int, ParagonIE_Sodium_Core32_Int64>
     */
    public $pad;

    /**
     * ParagonIE_Sodium_Core32_Poly1305_State constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $key
     * @throws InvalidArgumentException
     * @throws SodiumException
     * @throws TypeError
     */
    public function __construct($key = '')
    {
        if (self::strlen($key) < 32) {
            throw new InvalidArgumentException(
                'Poly1305 requires a 32-byte key'
            );
        }
        /* r &= 0xffffffc0ffffffc0ffffffc0fffffff */
        $this->r = array(
            // st->r[0] = ...
            ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 0, 4))
                ->setUnsignedInt(true)
                ->mask(0x3ffffff),
            // st->r[1] = ...
            ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 3, 4))
                ->setUnsignedInt(true)
                ->shiftRight(2)
                ->mask(0x3ffff03),
            // st->r[2] = ...
            ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 6, 4))
                ->setUnsignedInt(true)
                ->shiftRight(4)
                ->mask(0x3ffc0ff),
            // st->r[3] = ...
            ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 9, 4))
                ->setUnsignedInt(true)
                ->shiftRight(6)
                ->mask(0x3f03fff),
            // st->r[4] = ...
            ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 12, 4))
                ->setUnsignedInt(true)
                ->shiftRight(8)
                ->mask(0x00fffff)
        );

        /* h = 0 */
        $this->h = array(
            new ParagonIE_Sodium_Core32_Int32(array(0, 0), true),
            new ParagonIE_Sodium_Core32_Int32(array(0, 0), true),
            new ParagonIE_Sodium_Core32_Int32(array(0, 0), true),
            new ParagonIE_Sodium_Core32_Int32(array(0, 0), true),
            new ParagonIE_Sodium_Core32_Int32(array(0, 0), true)
        );

        /* save pad for later */
        $this->pad = array(
            ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 16, 4))
                ->setUnsignedInt(true)->toInt64(),
            ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 20, 4))
                ->setUnsignedInt(true)->toInt64(),
            ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 24, 4))
                ->setUnsignedInt(true)->toInt64(),
            ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 28, 4))
                ->setUnsignedInt(true)->toInt64(),
        );

        $this->leftover = 0;
        $this->final = false;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @return self
     * @throws SodiumException
     * @throws TypeError
     */
    public function update($message = '')
    {
        $bytes = self::strlen($message);

        /* handle leftover */
        if ($this->leftover) {
            /** @var int $want */
            $want = ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE - $this->leftover;
            if ($want > $bytes) {
                $want = $bytes;
            }
            for ($i = 0; $i < $want; ++$i) {
                $mi = self::chrToInt($message[$i]);
                $this->buffer[$this->leftover + $i] = $mi;
            }
            // We snip off the leftmost bytes.
            $message = self::substr($message, $want);
            $bytes = self::strlen($message);
            $this->leftover += $want;
            if ($this->leftover < ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE) {
                // We still don't have enough to run $this->blocks()
                return $this;
            }

            $this->blocks(
                self::intArrayToString($this->buffer),
                ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE
            );
            $this->leftover = 0;
        }

        /* process full blocks */
        if ($bytes >= ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE) {
            /** @var int $want */
            $want = $bytes & ~(ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE - 1);
            if ($want >= ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE) {
                /** @var string $block */
                $block = self::substr($message, 0, $want);
                if (self::strlen($block) >= ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE) {
                    $this->blocks($block, $want);
                    $message = self::substr($message, $want);
                    $bytes = self::strlen($message);
                }
            }
        }

        /* store leftover */
        if ($bytes) {
            for ($i = 0; $i < $bytes; ++$i) {
                $mi = self::chrToInt($message[$i]);
                $this->buffer[$this->leftover + $i] = $mi;
            }
            $this->leftover = (int) $this->leftover + $bytes;
        }
        return $this;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param int $bytes
     * @return self
     * @throws SodiumException
     * @throws TypeError
     */
    public function blocks($message, $bytes)
    {
        if (self::strlen($message) < 16) {
            $message = str_pad($message, 16, "\x00", STR_PAD_RIGHT);
        }
        $hibit = ParagonIE_Sodium_Core32_Int32::fromInt((int) ($this->final ? 0 : 1 << 24)); /* 1 << 128 */
        $hibit->setUnsignedInt(true);
        $zero = new ParagonIE_Sodium_Core32_Int64(array(0, 0, 0, 0), true);
        /**
         * @var ParagonIE_Sodium_Core32_Int64 $d0
         * @var ParagonIE_Sodium_Core32_Int64 $d1
         * @var ParagonIE_Sodium_Core32_Int64 $d2
         * @var ParagonIE_Sodium_Core32_Int64 $d3
         * @var ParagonIE_Sodium_Core32_Int64 $d4
         * @var ParagonIE_Sodium_Core32_Int64 $r0
         * @var ParagonIE_Sodium_Core32_Int64 $r1
         * @var ParagonIE_Sodium_Core32_Int64 $r2
         * @var ParagonIE_Sodium_Core32_Int64 $r3
         * @var ParagonIE_Sodium_Core32_Int64 $r4
         *
         * @var ParagonIE_Sodium_Core32_Int32 $h0
         * @var ParagonIE_Sodium_Core32_Int32 $h1
         * @var ParagonIE_Sodium_Core32_Int32 $h2
         * @var ParagonIE_Sodium_Core32_Int32 $h3
         * @var ParagonIE_Sodium_Core32_Int32 $h4
         */
        $r0 = $this->r[0]->toInt64();
        $r1 = $this->r[1]->toInt64();
        $r2 = $this->r[2]->toInt64();
        $r3 = $this->r[3]->toInt64();
        $r4 = $this->r[4]->toInt64();

        $s1 = $r1->toInt64()->mulInt(5, 3);
        $s2 = $r2->toInt64()->mulInt(5, 3);
        $s3 = $r3->toInt64()->mulInt(5, 3);
        $s4 = $r4->toInt64()->mulInt(5, 3);

        $h0 = $this->h[0];
        $h1 = $this->h[1];
        $h2 = $this->h[2];
        $h3 = $this->h[3];
        $h4 = $this->h[4];

        while ($bytes >= ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE) {
            /* h += m[i] */
            $h0 = $h0->addInt32(
                ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 0, 4))
                    ->mask(0x3ffffff)
            )->toInt64();
            $h1 = $h1->addInt32(
                ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 3, 4))
                    ->shiftRight(2)
                    ->mask(0x3ffffff)
            )->toInt64();
            $h2 = $h2->addInt32(
                ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 6, 4))
                    ->shiftRight(4)
                    ->mask(0x3ffffff)
            )->toInt64();
            $h3 = $h3->addInt32(
                ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 9, 4))
                    ->shiftRight(6)
                    ->mask(0x3ffffff)
            )->toInt64();
            $h4 = $h4->addInt32(
                ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($message, 12, 4))
                    ->shiftRight(8)
                    ->orInt32($hibit)
            )->toInt64();

            /* h *= r */
            $d0 = $zero
                ->addInt64($h0->mulInt64($r0, 25))
                ->addInt64($s4->mulInt64($h1, 26))
                ->addInt64($s3->mulInt64($h2, 26))
                ->addInt64($s2->mulInt64($h3, 26))
                ->addInt64($s1->mulInt64($h4, 26));

            $d1 = $zero
                ->addInt64($h0->mulInt64($r1, 25))
                ->addInt64($h1->mulInt64($r0, 25))
                ->addInt64($s4->mulInt64($h2, 26))
                ->addInt64($s3->mulInt64($h3, 26))
                ->addInt64($s2->mulInt64($h4, 26));

            $d2 = $zero
                ->addInt64($h0->mulInt64($r2, 25))
                ->addInt64($h1->mulInt64($r1, 25))
                ->addInt64($h2->mulInt64($r0, 25))
                ->addInt64($s4->mulInt64($h3, 26))
                ->addInt64($s3->mulInt64($h4, 26));

            $d3 = $zero
                ->addInt64($h0->mulInt64($r3, 25))
                ->addInt64($h1->mulInt64($r2, 25))
                ->addInt64($h2->mulInt64($r1, 25))
                ->addInt64($h3->mulInt64($r0, 25))
                ->addInt64($s4->mulInt64($h4, 26));

            $d4 = $zero
                ->addInt64($h0->mulInt64($r4, 25))
                ->addInt64($h1->mulInt64($r3, 25))
                ->addInt64($h2->mulInt64($r2, 25))
                ->addInt64($h3->mulInt64($r1, 25))
                ->addInt64($h4->mulInt64($r0, 25));

            /* (partial) h %= p */
            $c = $d0->shiftRight(26);
            $h0 = $d0->toInt32()->mask(0x3ffffff);
            $d1 = $d1->addInt64($c);

            $c = $d1->shiftRight(26);
            $h1 = $d1->toInt32()->mask(0x3ffffff);
            $d2 = $d2->addInt64($c);

            $c = $d2->shiftRight(26);
            $h2 = $d2->toInt32()->mask(0x3ffffff);
            $d3 = $d3->addInt64($c);

            $c = $d3->shiftRight(26);
            $h3 = $d3->toInt32()->mask(0x3ffffff);
            $d4 = $d4->addInt64($c);

            $c = $d4->shiftRight(26);
            $h4 = $d4->toInt32()->mask(0x3ffffff);
            $h0 = $h0->addInt32($c->toInt32()->mulInt(5, 3));

            $c = $h0->shiftRight(26);
            $h0 = $h0->mask(0x3ffffff);
            $h1 = $h1->addInt32($c);

            // Chop off the left 32 bytes.
            $message = self::substr(
                $message,
                ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE
            );
            $bytes -= ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE;
        }

        /** @var array<int, ParagonIE_Sodium_Core32_Int32> $h */
        $this->h = array($h0, $h1, $h2, $h3, $h4);
        return $this;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public function finish()
    {
        /* process the remaining block */
        if ($this->leftover) {
            $i = $this->leftover;
            $this->buffer[$i++] = 1;
            for (; $i < ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE; ++$i) {
                $this->buffer[$i] = 0;
            }
            $this->final = true;
            $this->blocks(
                self::substr(
                    self::intArrayToString($this->buffer),
                    0,
                    ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE
                ),
                $b = ParagonIE_Sodium_Core32_Poly1305::BLOCK_SIZE
            );
        }

        /**
         * @var ParagonIE_Sodium_Core32_Int32 $f
         * @var ParagonIE_Sodium_Core32_Int32 $g0
         * @var ParagonIE_Sodium_Core32_Int32 $g1
         * @var ParagonIE_Sodium_Core32_Int32 $g2
         * @var ParagonIE_Sodium_Core32_Int32 $g3
         * @var ParagonIE_Sodium_Core32_Int32 $g4
         * @var ParagonIE_Sodium_Core32_Int32 $h0
         * @var ParagonIE_Sodium_Core32_Int32 $h1
         * @var ParagonIE_Sodium_Core32_Int32 $h2
         * @var ParagonIE_Sodium_Core32_Int32 $h3
         * @var ParagonIE_Sodium_Core32_Int32 $h4
         */
        $h0 = $this->h[0];
        $h1 = $this->h[1];
        $h2 = $this->h[2];
        $h3 = $this->h[3];
        $h4 = $this->h[4];

        $c = $h1->shiftRight(26);           # $c = $h1 >> 26;
        $h1 = $h1->mask(0x3ffffff);         # $h1 &= 0x3ffffff;

        $h2 = $h2->addInt32($c);            # $h2 += $c;
        $c = $h2->shiftRight(26);           # $c = $h2 >> 26;
        $h2 = $h2->mask(0x3ffffff);         # $h2 &= 0x3ffffff;

        $h3 = $h3->addInt32($c);            # $h3 += $c;
        $c = $h3->shiftRight(26);           # $c = $h3 >> 26;
        $h3 = $h3->mask(0x3ffffff);         # $h3 &= 0x3ffffff;

        $h4 = $h4->addInt32($c);            # $h4 += $c;
        $c = $h4->shiftRight(26);           # $c = $h4 >> 26;
        $h4 = $h4->mask(0x3ffffff);         # $h4 &= 0x3ffffff;

        $h0 = $h0->addInt32($c->mulInt(5, 3)); # $h0 += self::mul($c, 5);
        $c = $h0->shiftRight(26);           # $c = $h0 >> 26;
        $h0 = $h0->mask(0x3ffffff);         # $h0 &= 0x3ffffff;
        $h1 = $h1->addInt32($c);            # $h1 += $c;

        /* compute h + -p */
        $g0 = $h0->addInt(5);
        $c  = $g0->shiftRight(26);
        $g0 = $g0->mask(0x3ffffff);
        $g1 = $h1->addInt32($c);
        $c  = $g1->shiftRight(26);
        $g1 = $g1->mask(0x3ffffff);
        $g2 = $h2->addInt32($c);
        $c  = $g2->shiftRight(26);
        $g2 = $g2->mask(0x3ffffff);
        $g3 = $h3->addInt32($c);
        $c  = $g3->shiftRight(26);
        $g3 = $g3->mask(0x3ffffff);
        $g4 = $h4->addInt32($c)->subInt(1 << 26);

        # $mask = ($g4 >> 31) - 1;
        /* select h if h < p, or h + -p if h >= p */
        $mask = (int) (($g4->toInt() >> 31) + 1);

        $g0 = $g0->mask($mask);
        $g1 = $g1->mask($mask);
        $g2 = $g2->mask($mask);
        $g3 = $g3->mask($mask);
        $g4 = $g4->mask($mask);

        /** @var int $mask */
        $mask = (~$mask) & 0xffffffff;

        $h0 = $h0->mask($mask)->orInt32($g0);
        $h1 = $h1->mask($mask)->orInt32($g1);
        $h2 = $h2->mask($mask)->orInt32($g2);
        $h3 = $h3->mask($mask)->orInt32($g3);
        $h4 = $h4->mask($mask)->orInt32($g4);

        /* h = h % (2^128) */
        $h0 = $h0->orInt32($h1->shiftLeft(26));
        $h1 = $h1->shiftRight(6)->orInt32($h2->shiftLeft(20));
        $h2 = $h2->shiftRight(12)->orInt32($h3->shiftLeft(14));
        $h3 = $h3->shiftRight(18)->orInt32($h4->shiftLeft(8));

        /* mac = (h + pad) % (2^128) */
        $f = $h0->toInt64()->addInt64($this->pad[0]);
        $h0 = $f->toInt32();
        $f = $h1->toInt64()->addInt64($this->pad[1])->addInt($h0->overflow);
        $h1 = $f->toInt32();
        $f = $h2->toInt64()->addInt64($this->pad[2])->addInt($h1->overflow);
        $h2 = $f->toInt32();
        $f = $h3->toInt64()->addInt64($this->pad[3])->addInt($h2->overflow);
        $h3 = $f->toInt32();

        return $h0->toReverseString() .
            $h1->toReverseString() .
            $h2->toReverseString() .
            $h3->toReverseString();
    }
}
Core32/HChaCha20.php000064400000012261152331075510007677 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_HChaCha20', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_HChaCha20
 */
class ParagonIE_Sodium_Core32_HChaCha20 extends ParagonIE_Sodium_Core32_ChaCha20
{
    /**
     * @param string $in
     * @param string $key
     * @param string|null $c
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function hChaCha20($in = '', $key = '', $c = null)
    {
        $ctx = array();

        if ($c === null) {
            $ctx[0] = new ParagonIE_Sodium_Core32_Int32(array(0x6170, 0x7865));
            $ctx[1] = new ParagonIE_Sodium_Core32_Int32(array(0x3320, 0x646e));
            $ctx[2] = new ParagonIE_Sodium_Core32_Int32(array(0x7962, 0x2d32));
            $ctx[3] = new ParagonIE_Sodium_Core32_Int32(array(0x6b20, 0x6574));
        } else {
            $ctx[0] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($c, 0, 4));
            $ctx[1] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($c, 4, 4));
            $ctx[2] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($c, 8, 4));
            $ctx[3] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($c, 12, 4));
        }
        $ctx[4]  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 0, 4));
        $ctx[5]  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 4, 4));
        $ctx[6]  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 8, 4));
        $ctx[7]  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 12, 4));
        $ctx[8]  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 16, 4));
        $ctx[9]  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 20, 4));
        $ctx[10] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 24, 4));
        $ctx[11] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 28, 4));
        $ctx[12] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($in, 0, 4));
        $ctx[13] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($in, 4, 4));
        $ctx[14] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($in, 8, 4));
        $ctx[15] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($in, 12, 4));

        return self::hChaCha20Bytes($ctx);
    }

    /**
     * @param array $ctx
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function hChaCha20Bytes(array $ctx)
    {
        /** @var ParagonIE_Sodium_Core32_Int32 $x0 */
        $x0  = $ctx[0];
        /** @var ParagonIE_Sodium_Core32_Int32 $x1 */
        $x1  = $ctx[1];
        /** @var ParagonIE_Sodium_Core32_Int32 $x2 */
        $x2  = $ctx[2];
        /** @var ParagonIE_Sodium_Core32_Int32 $x3 */
        $x3  = $ctx[3];
        /** @var ParagonIE_Sodium_Core32_Int32 $x4 */
        $x4  = $ctx[4];
        /** @var ParagonIE_Sodium_Core32_Int32 $x5 */
        $x5  = $ctx[5];
        /** @var ParagonIE_Sodium_Core32_Int32 $x6 */
        $x6  = $ctx[6];
        /** @var ParagonIE_Sodium_Core32_Int32 $x7 */
        $x7  = $ctx[7];
        /** @var ParagonIE_Sodium_Core32_Int32 $x8 */
        $x8  = $ctx[8];
        /** @var ParagonIE_Sodium_Core32_Int32 $x9 */
        $x9  = $ctx[9];
        /** @var ParagonIE_Sodium_Core32_Int32 $x10 */
        $x10 = $ctx[10];
        /** @var ParagonIE_Sodium_Core32_Int32 $x11 */
        $x11 = $ctx[11];
        /** @var ParagonIE_Sodium_Core32_Int32 $x12 */
        $x12 = $ctx[12];
        /** @var ParagonIE_Sodium_Core32_Int32 $x13 */
        $x13 = $ctx[13];
        /** @var ParagonIE_Sodium_Core32_Int32 $x14 */
        $x14 = $ctx[14];
        /** @var ParagonIE_Sodium_Core32_Int32 $x15 */
        $x15 = $ctx[15];

        for ($i = 0; $i < 10; ++$i) {
            # QUARTERROUND( x0,  x4,  x8,  x12)
            list($x0, $x4, $x8, $x12) = self::quarterRound($x0, $x4, $x8, $x12);

            # QUARTERROUND( x1,  x5,  x9,  x13)
            list($x1, $x5, $x9, $x13) = self::quarterRound($x1, $x5, $x9, $x13);

            # QUARTERROUND( x2,  x6,  x10,  x14)
            list($x2, $x6, $x10, $x14) = self::quarterRound($x2, $x6, $x10, $x14);

            # QUARTERROUND( x3,  x7,  x11,  x15)
            list($x3, $x7, $x11, $x15) = self::quarterRound($x3, $x7, $x11, $x15);

            # QUARTERROUND( x0,  x5,  x10,  x15)
            list($x0, $x5, $x10, $x15) = self::quarterRound($x0, $x5, $x10, $x15);

            # QUARTERROUND( x1,  x6,  x11,  x12)
            list($x1, $x6, $x11, $x12) = self::quarterRound($x1, $x6, $x11, $x12);

            # QUARTERROUND( x2,  x7,  x8,  x13)
            list($x2, $x7, $x8, $x13) = self::quarterRound($x2, $x7, $x8, $x13);

            # QUARTERROUND( x3,  x4,  x9,  x14)
            list($x3, $x4, $x9, $x14) = self::quarterRound($x3, $x4, $x9, $x14);
        }

        return $x0->toReverseString() .
            $x1->toReverseString() .
            $x2->toReverseString() .
            $x3->toReverseString() .
            $x12->toReverseString() .
            $x13->toReverseString() .
            $x14->toReverseString() .
            $x15->toReverseString();
    }
}
Core32/Ed25519.php000064400000036402152331075510007257 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_Ed25519', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_Ed25519
 */
abstract class ParagonIE_Sodium_Core32_Ed25519 extends ParagonIE_Sodium_Core32_Curve25519
{
    const KEYPAIR_BYTES = 96;
    const SEED_BYTES = 32;

    /**
     * @internal You should not use this directly from another application
     *
     * @return string (96 bytes)
     * @throws Exception
     * @throws SodiumException
     * @throws TypeError
     */
    public static function keypair()
    {
        $seed = random_bytes(self::SEED_BYTES);
        $pk = '';
        $sk = '';
        self::seed_keypair($pk, $sk, $seed);
        return $sk . $pk;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $pk
     * @param string $sk
     * @param string $seed
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function seed_keypair(&$pk, &$sk, $seed)
    {
        if (self::strlen($seed) !== self::SEED_BYTES) {
            throw new RangeException('crypto_sign keypair seed must be 32 bytes long');
        }

        /** @var string $pk */
        $pk = self::publickey_from_secretkey($seed);
        $sk = $seed . $pk;
        return $sk;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $keypair
     * @return string
     * @throws TypeError
     */
    public static function secretkey($keypair)
    {
        if (self::strlen($keypair) !== self::KEYPAIR_BYTES) {
            throw new RangeException('crypto_sign keypair must be 96 bytes long');
        }
        return self::substr($keypair, 0, 64);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $keypair
     * @return string
     * @throws RangeException
     * @throws TypeError
     */
    public static function publickey($keypair)
    {
        if (self::strlen($keypair) !== self::KEYPAIR_BYTES) {
            throw new RangeException('crypto_sign keypair must be 96 bytes long');
        }
        return self::substr($keypair, 64, 32);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function publickey_from_secretkey($sk)
    {
        /** @var string $sk */
        $sk = hash('sha512', self::substr($sk, 0, 32), true);
        $sk[0] = self::intToChr(
            self::chrToInt($sk[0]) & 248
        );
        $sk[31] = self::intToChr(
            (self::chrToInt($sk[31]) & 63) | 64
        );
        return self::sk_to_pk($sk);
    }

    /**
     * @param string $pk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function pk_to_curve25519($pk)
    {
        if (self::small_order($pk)) {
            throw new SodiumException('Public key is on a small order');
        }
        $A = self::ge_frombytes_negate_vartime($pk);
        $p1 = self::ge_mul_l($A);
        if (!self::fe_isnonzero($p1->X)) {
            throw new SodiumException('Unexpected zero result');
        }

        # fe_1(one_minus_y);
        # fe_sub(one_minus_y, one_minus_y, A.Y);
        # fe_invert(one_minus_y, one_minus_y);
        $one_minux_y = self::fe_invert(
            self::fe_sub(
                self::fe_1(),
                $A->Y
            )
        );


        # fe_1(x);
        # fe_add(x, x, A.Y);
        # fe_mul(x, x, one_minus_y);
        $x = self::fe_mul(
            self::fe_add(self::fe_1(), $A->Y),
            $one_minux_y
        );

        # fe_tobytes(curve25519_pk, x);
        return self::fe_tobytes($x);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sk_to_pk($sk)
    {
        return self::ge_p3_tobytes(
            self::ge_scalarmult_base(
                self::substr($sk, 0, 32)
            )
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign($message, $sk)
    {
        /** @var string $signature */
        $signature = self::sign_detached($message, $sk);
        return $signature . $message;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message A signed message
     * @param string $pk      Public key
     * @return string         Message (without signature)
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign_open($message, $pk)
    {
        /** @var string $signature */
        $signature = self::substr($message, 0, 64);

        /** @var string $message */
        $message = self::substr($message, 64);

        if (self::verify_detached($signature, $message, $pk)) {
            return $message;
        }
        throw new SodiumException('Invalid signature');
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress PossiblyInvalidArgument
     */
    public static function sign_detached($message, $sk)
    {
        # crypto_hash_sha512(az, sk, 32);
        $az =  hash('sha512', self::substr($sk, 0, 32), true);

        # az[0] &= 248;
        # az[31] &= 63;
        # az[31] |= 64;
        $az[0] = self::intToChr(self::chrToInt($az[0]) & 248);
        $az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64);

        # crypto_hash_sha512_init(&hs);
        # crypto_hash_sha512_update(&hs, az + 32, 32);
        # crypto_hash_sha512_update(&hs, m, mlen);
        # crypto_hash_sha512_final(&hs, nonce);
        $hs = hash_init('sha512');
        self::hash_update($hs, self::substr($az, 32, 32));
        self::hash_update($hs, $message);
        $nonceHash = hash_final($hs, true);

        # memmove(sig + 32, sk + 32, 32);
        $pk = self::substr($sk, 32, 32);

        # sc_reduce(nonce);
        # ge_scalarmult_base(&R, nonce);
        # ge_p3_tobytes(sig, &R);
        $nonce = self::sc_reduce($nonceHash) . self::substr($nonceHash, 32);
        $sig = self::ge_p3_tobytes(
            self::ge_scalarmult_base($nonce)
        );

        # crypto_hash_sha512_init(&hs);
        # crypto_hash_sha512_update(&hs, sig, 64);
        # crypto_hash_sha512_update(&hs, m, mlen);
        # crypto_hash_sha512_final(&hs, hram);
        $hs = hash_init('sha512');
        self::hash_update($hs, self::substr($sig, 0, 32));
        self::hash_update($hs, self::substr($pk, 0, 32));
        self::hash_update($hs, $message);
        $hramHash = hash_final($hs, true);

        # sc_reduce(hram);
        # sc_muladd(sig + 32, hram, az, nonce);
        $hram = self::sc_reduce($hramHash);
        $sigAfter = self::sc_muladd($hram, $az, $nonce);
        $sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32);

        try {
            ParagonIE_Sodium_Compat::memzero($az);
        } catch (SodiumException $ex) {
            $az = null;
        }
        return $sig;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $sig
     * @param string $message
     * @param string $pk
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function verify_detached($sig, $message, $pk)
    {
        if (self::strlen($sig) < 64) {
            throw new SodiumException('Signature is too short');
        }
        if ((self::chrToInt($sig[63]) & 240) && self::check_S_lt_L(self::substr($sig, 32, 32))) {
            throw new SodiumException('S < L - Invalid signature');
        }
        if (self::small_order($sig)) {
            throw new SodiumException('Signature is on too small of an order');
        }
        if ((self::chrToInt($sig[63]) & 224) !== 0) {
            throw new SodiumException('Invalid signature');
        }
        $d = 0;
        for ($i = 0; $i < 32; ++$i) {
            $d |= self::chrToInt($pk[$i]);
        }
        if ($d === 0) {
            throw new SodiumException('All zero public key');
        }

        /** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */
        $orig = ParagonIE_Sodium_Compat::$fastMult;

        // Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification.
        ParagonIE_Sodium_Compat::$fastMult = true;

        /** @var ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $A */
        $A = self::ge_frombytes_negate_vartime($pk);

        /** @var string $hDigest */
        $hDigest = hash(
            'sha512',
            self::substr($sig, 0, 32) .
            self::substr($pk, 0, 32) .
            $message,
            true
        );

        /** @var string $h */
        $h = self::sc_reduce($hDigest) . self::substr($hDigest, 32);

        /** @var ParagonIE_Sodium_Core32_Curve25519_Ge_P2 $R */
        $R = self::ge_double_scalarmult_vartime(
            $h,
            $A,
            self::substr($sig, 32)
        );

        /** @var string $rcheck */
        $rcheck = self::ge_tobytes($R);

        // Reset ParagonIE_Sodium_Compat::$fastMult to what it was before.
        ParagonIE_Sodium_Compat::$fastMult = $orig;

        return self::verify_32($rcheck, self::substr($sig, 0, 32));
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $S
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function check_S_lt_L($S)
    {
        if (self::strlen($S) < 32) {
            throw new SodiumException('Signature must be 32 bytes');
        }
        static $L = array(
            0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,
            0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10
        );
        /** @var array<int, int> $L */
        $c = 0;
        $n = 1;
        $i = 32;

        do {
            --$i;
            $x = self::chrToInt($S[$i]);
            $c |= (
                (($x - $L[$i]) >> 8) & $n
            );
            $n &= (
                (($x ^ $L[$i]) - 1) >> 8
            );
        } while ($i !== 0);

        return $c === 0;
    }

    /**
     * @param string $R
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function small_order($R)
    {
        static $blocklist = array(
            /* 0 (order 4) */
            array(
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
            ),
            /* 1 (order 1) */
            array(
                0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
            ),
            /* 2707385501144840649318225287225658788936804267575313519463743609750303402022 (order 8) */
            array(
                0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0,
                0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98, 0xf0,
                0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39,
                0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05
            ),
            /* 55188659117513257062467267217118295137698188065244968500265048394206261417927 (order 8) */
            array(
                0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f,
                0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, 0x0f,
                0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6,
                0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a
            ),
            /* p-1 (order 2) */
            array(
                0x13, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0,
                0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98, 0xf0,
                0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39,
                0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x85
            ),
            /* p (order 4) */
            array(
                0xb4, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f,
                0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, 0x0f,
                0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6,
                0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0xfa
            ),
            /* p+1 (order 1) */
            array(
                0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f
            ),
            /* p+2707385501144840649318225287225658788936804267575313519463743609750303402022 (order 8) */
            array(
                0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f
            ),
            /* p+55188659117513257062467267217118295137698188065244968500265048394206261417927 (order 8) */
            array(
                0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f
            ),
            /* 2p-1 (order 2) */
            array(
                0xd9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
            ),
            /* 2p (order 4) */
            array(
                0xda, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
            ),
            /* 2p+1 (order 1) */
            array(
                0xdb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
            )
        );
        /** @var array<int, array<int, int>> $blocklist */
        $countBlocklist = count($blocklist);

        for ($i = 0; $i < $countBlocklist; ++$i) {
            $c = 0;
            for ($j = 0; $j < 32; ++$j) {
                $c |= self::chrToInt($R[$j]) ^ $blocklist[$i][$j];
            }
            if ($c === 0) {
                return true;
            }
        }
        return false;
    }
}
Core32/Util.php000064400000000321152331075510007225 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_Util', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_Util
 */
abstract class ParagonIE_Sodium_Core32_Util extends ParagonIE_Sodium_Core_Util
{

}
Core32/XChaCha20.php000064400000003357152331075510007725 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_XChaCha20', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_XChaCha20
 */
class ParagonIE_Sodium_Core32_XChaCha20 extends ParagonIE_Sodium_Core32_HChaCha20
{
    /**
     * @internal You should not use this directly from another application
     *
     * @param int $len
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function stream($len = 64, $nonce = '', $key = '')
    {
        if (self::strlen($nonce) !== 24) {
            throw new SodiumException('Nonce must be 24 bytes long');
        }
        return self::encryptBytes(
            new ParagonIE_Sodium_Core32_ChaCha20_Ctx(
                self::hChaCha20(
                    self::substr($nonce, 0, 16),
                    $key
                ),
                self::substr($nonce, 16, 8)
            ),
            str_repeat("\x00", $len)
        );
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $nonce
     * @param string $key
     * @param string $ic
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function streamXorIc($message, $nonce = '', $key = '', $ic = '')
    {
        if (self::strlen($nonce) !== 24) {
            throw new SodiumException('Nonce must be 24 bytes long');
        }
        return self::encryptBytes(
            new ParagonIE_Sodium_Core32_ChaCha20_Ctx(
                self::hChaCha20(self::substr($nonce, 0, 16), $key),
                self::substr($nonce, 16, 8),
                $ic
            ),
            $message
        );
    }
}
Core32/BLAKE2b.php000064400000053464152331075510007372 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_BLAKE2b', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core_BLAKE2b
 *
 * Based on the work of Devi Mandiri in devi/salt.
 */
abstract class ParagonIE_Sodium_Core32_BLAKE2b extends ParagonIE_Sodium_Core_Util
{
    /**
     * @var SplFixedArray
     */
    public static $iv;

    /**
     * @var array<int, array<int, int>>
     */
    public static $sigma = array(
        array(  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15),
        array( 14, 10,  4,  8,  9, 15, 13,  6,  1, 12,  0,  2, 11,  7,  5,  3),
        array( 11,  8, 12,  0,  5,  2, 15, 13, 10, 14,  3,  6,  7,  1,  9,  4),
        array(  7,  9,  3,  1, 13, 12, 11, 14,  2,  6,  5, 10,  4,  0, 15,  8),
        array(  9,  0,  5,  7,  2,  4, 10, 15, 14,  1, 11, 12,  6,  8,  3, 13),
        array(  2, 12,  6, 10,  0, 11,  8,  3,  4, 13,  7,  5, 15, 14,  1,  9),
        array( 12,  5,  1, 15, 14, 13,  4, 10,  0,  7,  6,  3,  9,  2,  8, 11),
        array( 13, 11,  7, 14, 12,  1,  3,  9,  5,  0, 15,  4,  8,  6,  2, 10),
        array(  6, 15, 14,  9, 11,  3,  0,  8, 12,  2, 13,  7,  1,  4, 10,  5),
        array( 10,  2,  8,  4,  7,  6,  1,  5, 15, 11,  9, 14,  3, 12, 13 , 0),
        array(  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15),
        array( 14, 10,  4,  8,  9, 15, 13,  6,  1, 12,  0,  2, 11,  7,  5,  3)
    );

    const BLOCKBYTES = 128;
    const OUTBYTES   = 64;
    const KEYBYTES   = 64;

    /**
     * Turn two 32-bit integers into a fixed array representing a 64-bit integer.
     *
     * @internal You should not use this directly from another application
     *
     * @param int $high
     * @param int $low
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws SodiumException
     * @throws TypeError
     */
    public static function new64($high, $low)
    {
        return ParagonIE_Sodium_Core32_Int64::fromInts($low, $high);
    }

    /**
     * Convert an arbitrary number into an SplFixedArray of two 32-bit integers
     * that represents a 64-bit integer.
     *
     * @internal You should not use this directly from another application
     *
     * @param int $num
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function to64($num)
    {
        list($hi, $lo) = self::numericTo64BitInteger($num);
        return self::new64($hi, $lo);
    }

    /**
     * Adds two 64-bit integers together, returning their sum as a SplFixedArray
     * containing two 32-bit integers (representing a 64-bit integer).
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Int64 $x
     * @param ParagonIE_Sodium_Core32_Int64 $y
     * @return ParagonIE_Sodium_Core32_Int64
     */
    protected static function add64($x, $y)
    {
        return $x->addInt64($y);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Int64 $x
     * @param ParagonIE_Sodium_Core32_Int64 $y
     * @param ParagonIE_Sodium_Core32_Int64 $z
     * @return ParagonIE_Sodium_Core32_Int64
     */
    public static function add364($x, $y, $z)
    {
        return $x->addInt64($y)->addInt64($z);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Int64 $x
     * @param ParagonIE_Sodium_Core32_Int64 $y
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws TypeError
     */
    public static function xor64(ParagonIE_Sodium_Core32_Int64 $x, ParagonIE_Sodium_Core32_Int64 $y)
    {
        return $x->xorInt64($y);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Int64 $x
     * @param int $c
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws SodiumException
     * @throws TypeError
     */
    public static function rotr64(ParagonIE_Sodium_Core32_Int64 $x, $c)
    {
        return $x->rotateRight($c);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $x
     * @param int $i
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws SodiumException
     * @throws TypeError
     */
    public static function load64($x, $i)
    {
        /** @var int $l */
        $l = (int) ($x[$i])
             | ((int) ($x[$i+1]) << 8)
             | ((int) ($x[$i+2]) << 16)
             | ((int) ($x[$i+3]) << 24);
        /** @var int $h */
        $h = (int) ($x[$i+4])
             | ((int) ($x[$i+5]) << 8)
             | ((int) ($x[$i+6]) << 16)
             | ((int) ($x[$i+7]) << 24);
        return self::new64($h, $l);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $x
     * @param int $i
     * @param ParagonIE_Sodium_Core32_Int64 $u
     * @return void
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     * @psalm-suppress MixedArrayOffset
     */
    public static function store64(SplFixedArray $x, $i, ParagonIE_Sodium_Core32_Int64 $u)
    {
        $v = clone $u;
        $maxLength = $x->getSize() - 1;
        for ($j = 0; $j < 8; ++$j) {
            $k = 3 - ($j >> 1);
            $x[$i] = $v->limbs[$k] & 0xff;
            if (++$i > $maxLength) {
                return;
            }
            $v->limbs[$k] >>= 8;
        }
    }

    /**
     * This just sets the $iv static variable.
     *
     * @internal You should not use this directly from another application
     *
     * @return void
     * @throws SodiumException
     * @throws TypeError
     */
    public static function pseudoConstructor()
    {
        static $called = false;
        if ($called) {
            return;
        }
        self::$iv = new SplFixedArray(8);
        self::$iv[0] = self::new64(0x6a09e667, 0xf3bcc908);
        self::$iv[1] = self::new64(0xbb67ae85, 0x84caa73b);
        self::$iv[2] = self::new64(0x3c6ef372, 0xfe94f82b);
        self::$iv[3] = self::new64(0xa54ff53a, 0x5f1d36f1);
        self::$iv[4] = self::new64(0x510e527f, 0xade682d1);
        self::$iv[5] = self::new64(0x9b05688c, 0x2b3e6c1f);
        self::$iv[6] = self::new64(0x1f83d9ab, 0xfb41bd6b);
        self::$iv[7] = self::new64(0x5be0cd19, 0x137e2179);

        $called = true;
    }

    /**
     * Returns a fresh BLAKE2 context.
     *
     * @internal You should not use this directly from another application
     *
     * @return SplFixedArray
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     * @psalm-suppress MixedArrayOffset
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function context()
    {
        $ctx    = new SplFixedArray(6);
        $ctx[0] = new SplFixedArray(8);   // h
        $ctx[1] = new SplFixedArray(2);   // t
        $ctx[2] = new SplFixedArray(2);   // f
        $ctx[3] = new SplFixedArray(256); // buf
        $ctx[4] = 0;                      // buflen
        $ctx[5] = 0;                      // last_node (uint8_t)

        for ($i = 8; $i--;) {
            $ctx[0][$i] = self::$iv[$i];
        }
        for ($i = 256; $i--;) {
            $ctx[3][$i] = 0;
        }

        $zero = self::new64(0, 0);
        $ctx[1][0] = $zero;
        $ctx[1][1] = $zero;
        $ctx[2][0] = $zero;
        $ctx[2][1] = $zero;

        return $ctx;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $ctx
     * @param SplFixedArray $buf
     * @return void
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     * @psalm-suppress MixedAssignment
     */
    protected static function compress(SplFixedArray $ctx, SplFixedArray $buf)
    {
        $m = new SplFixedArray(16);
        $v = new SplFixedArray(16);

        for ($i = 16; $i--;) {
            $m[$i] = self::load64($buf, $i << 3);
        }

        for ($i = 8; $i--;) {
            $v[$i] = $ctx[0][$i];
        }

        $v[ 8] = self::$iv[0];
        $v[ 9] = self::$iv[1];
        $v[10] = self::$iv[2];
        $v[11] = self::$iv[3];

        $v[12] = self::xor64($ctx[1][0], self::$iv[4]);
        $v[13] = self::xor64($ctx[1][1], self::$iv[5]);
        $v[14] = self::xor64($ctx[2][0], self::$iv[6]);
        $v[15] = self::xor64($ctx[2][1], self::$iv[7]);

        for ($r = 0; $r < 12; ++$r) {
            $v = self::G($r, 0, 0, 4, 8, 12, $v, $m);
            $v = self::G($r, 1, 1, 5, 9, 13, $v, $m);
            $v = self::G($r, 2, 2, 6, 10, 14, $v, $m);
            $v = self::G($r, 3, 3, 7, 11, 15, $v, $m);
            $v = self::G($r, 4, 0, 5, 10, 15, $v, $m);
            $v = self::G($r, 5, 1, 6, 11, 12, $v, $m);
            $v = self::G($r, 6, 2, 7, 8, 13, $v, $m);
            $v = self::G($r, 7, 3, 4, 9, 14, $v, $m);
        }

        for ($i = 8; $i--;) {
            $ctx[0][$i] = self::xor64(
                $ctx[0][$i], self::xor64($v[$i], $v[$i+8])
            );
        }
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $r
     * @param int $i
     * @param int $a
     * @param int $b
     * @param int $c
     * @param int $d
     * @param SplFixedArray $v
     * @param SplFixedArray $m
     * @return SplFixedArray
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedArrayOffset
     */
    public static function G($r, $i, $a, $b, $c, $d, SplFixedArray $v, SplFixedArray $m)
    {
        $v[$a] = self::add364($v[$a], $v[$b], $m[self::$sigma[$r][$i << 1]]);
        $v[$d] = self::rotr64(self::xor64($v[$d], $v[$a]), 32);
        $v[$c] = self::add64($v[$c], $v[$d]);
        $v[$b] = self::rotr64(self::xor64($v[$b], $v[$c]), 24);
        $v[$a] = self::add364($v[$a], $v[$b], $m[self::$sigma[$r][($i << 1) + 1]]);
        $v[$d] = self::rotr64(self::xor64($v[$d], $v[$a]), 16);
        $v[$c] = self::add64($v[$c], $v[$d]);
        $v[$b] = self::rotr64(self::xor64($v[$b], $v[$c]), 63);
        return $v;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $ctx
     * @param int $inc
     * @return void
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     */
    public static function increment_counter($ctx, $inc)
    {
        if ($inc < 0) {
            throw new SodiumException('Increasing by a negative number makes no sense.');
        }
        $t = self::to64($inc);
        # S->t is $ctx[1] in our implementation

        # S->t[0] = ( uint64_t )( t >> 0 );
        $ctx[1][0] = self::add64($ctx[1][0], $t);

        # S->t[1] += ( S->t[0] < inc );
        if (!($ctx[1][0] instanceof ParagonIE_Sodium_Core32_Int64)) {
            throw new TypeError('Not an int64');
        }
        /** @var ParagonIE_Sodium_Core32_Int64 $c*/
        $c = $ctx[1][0];
        if ($c->isLessThanInt($inc)) {
            $ctx[1][1] = self::add64($ctx[1][1], self::to64(1));
        }
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $ctx
     * @param SplFixedArray $p
     * @param int $plen
     * @return void
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     * @psalm-suppress MixedArrayOffset
     * @psalm-suppress MixedMethodCall
     * @psalm-suppress MixedOperand
     */
    public static function update(SplFixedArray $ctx, SplFixedArray $p, $plen)
    {
        self::pseudoConstructor();

        $offset = 0;
        while ($plen > 0) {
            $left = $ctx[4];
            $fill = 256 - $left;

            if ($plen > $fill) {
                # memcpy( S->buf + left, in, fill ); /* Fill buffer */
                for ($i = $fill; $i--;) {
                    $ctx[3][$i + $left] = $p[$i + $offset];
                }

                # S->buflen += fill;
                $ctx[4] += $fill;

                # blake2b_increment_counter( S, BLAKE2B_BLOCKBYTES );
                self::increment_counter($ctx, 128);

                # blake2b_compress( S, S->buf ); /* Compress */
                self::compress($ctx, $ctx[3]);

                # memcpy( S->buf, S->buf + BLAKE2B_BLOCKBYTES, BLAKE2B_BLOCKBYTES ); /* Shift buffer left */
                for ($i = 128; $i--;) {
                    $ctx[3][$i] = $ctx[3][$i + 128];
                }

                # S->buflen -= BLAKE2B_BLOCKBYTES;
                $ctx[4] -= 128;

                # in += fill;
                $offset += $fill;

                # inlen -= fill;
                $plen -= $fill;
            } else {
                for ($i = $plen; $i--;) {
                    $ctx[3][$i + $left] = $p[$i + $offset];
                }
                $ctx[4] += $plen;
                $offset += $plen;
                $plen -= $plen;
            }
        }
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $ctx
     * @param SplFixedArray $out
     * @return SplFixedArray
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     * @psalm-suppress MixedArrayOffset
     * @psalm-suppress MixedMethodCall
     * @psalm-suppress MixedOperand
     */
    public static function finish(SplFixedArray $ctx, SplFixedArray $out)
    {
        self::pseudoConstructor();
        if ($ctx[4] > 128) {
            self::increment_counter($ctx, 128);
            self::compress($ctx, $ctx[3]);
            $ctx[4] -= 128;
            if ($ctx[4] > 128) {
                throw new SodiumException('Failed to assert that buflen <= 128 bytes');
            }
            for ($i = $ctx[4]; $i--;) {
                $ctx[3][$i] = $ctx[3][$i + 128];
            }
        }

        self::increment_counter($ctx, $ctx[4]);
        $ctx[2][0] = self::new64(0xffffffff, 0xffffffff);

        for ($i = 256 - $ctx[4]; $i--;) {
            /** @var int $i */
            $ctx[3][$i + $ctx[4]] = 0;
        }

        self::compress($ctx, $ctx[3]);

        $i = (int) (($out->getSize() - 1) / 8);
        for (; $i >= 0; --$i) {
            self::store64($out, $i << 3, $ctx[0][$i]);
        }
        return $out;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray|null $key
     * @param int $outlen
     * @param SplFixedArray|null $salt
     * @param SplFixedArray|null $personal
     * @return SplFixedArray
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     * @psalm-suppress MixedMethodCall
     */
    public static function init(
        $key = null,
        $outlen = 64,
        $salt = null,
        $personal = null
    ) {
        self::pseudoConstructor();
        $klen = 0;

        if ($key !== null) {
            if (count($key) > 64) {
                throw new SodiumException('Invalid key size');
            }
            $klen = count($key);
        }

        if ($outlen > 64) {
            throw new SodiumException('Invalid output size');
        }

        $ctx = self::context();

        $p = new SplFixedArray(64);
        // Zero our param buffer...
        for ($i = 64; --$i;) {
            $p[$i] = 0;
        }

        $p[0] = $outlen; // digest_length
        $p[1] = $klen;   // key_length
        $p[2] = 1;       // fanout
        $p[3] = 1;       // depth

        if ($salt instanceof SplFixedArray) {
            // salt: [32] through [47]
            for ($i = 0; $i < 16; ++$i) {
                $p[32 + $i] = (int) $salt[$i];
            }
        }
        if ($personal instanceof SplFixedArray) {
            // personal: [48] through [63]
            for ($i = 0; $i < 16; ++$i) {
                $p[48 + $i] = (int) $personal[$i];
            }
        }

        $ctx[0][0] = self::xor64(
            $ctx[0][0],
            self::load64($p, 0)
        );

        if ($salt instanceof SplFixedArray || $personal instanceof SplFixedArray) {
            // We need to do what blake2b_init_param() does:
            for ($i = 1; $i < 8; ++$i) {
                $ctx[0][$i] = self::xor64(
                    $ctx[0][$i],
                    self::load64($p, $i << 3)
                );
            }
        }

        if ($klen > 0 && $key instanceof SplFixedArray) {
            $block = new SplFixedArray(128);
            for ($i = 128; $i--;) {
                $block[$i] = 0;
            }
            for ($i = $klen; $i--;) {
                $block[$i] = $key[$i];
            }
            self::update($ctx, $block, 128);
            $ctx[4] = 128;
        }

        return $ctx;
    }

    /**
     * Convert a string into an SplFixedArray of integers
     *
     * @internal You should not use this directly from another application
     *
     * @param string $str
     * @return SplFixedArray
     * @psalm-suppress MixedArgumentTypeCoercion
     */
    public static function stringToSplFixedArray($str = '')
    {
        $values = unpack('C*', $str);
        return SplFixedArray::fromArray(array_values($values));
    }

    /**
     * Convert an SplFixedArray of integers into a string
     *
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $a
     * @return string
     */
    public static function SplFixedArrayToString(SplFixedArray $a)
    {
        /**
         * @var array<int, string|int>
         */
        $arr = $a->toArray();
        $c = $a->count();
        array_unshift($arr, str_repeat('C', $c));
        return (string) (call_user_func_array('pack', $arr));
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param SplFixedArray $ctx
     * @return string
     * @throws TypeError
     * @psalm-suppress MixedArgument
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     * @psalm-suppress MixedMethodCall
     */
    public static function contextToString(SplFixedArray $ctx)
    {
        $str = '';
        /** @var array<int, ParagonIE_Sodium_Core32_Int64> $ctxA */
        $ctxA = $ctx[0]->toArray();

        # uint64_t h[8];
        for ($i = 0; $i < 8; ++$i) {
            if (!($ctxA[$i] instanceof ParagonIE_Sodium_Core32_Int64)) {
                throw new TypeError('Not an instance of Int64');
            }
            /** @var ParagonIE_Sodium_Core32_Int64 $ctxAi */
            $ctxAi = $ctxA[$i];
            $str .= $ctxAi->toReverseString();
        }

        # uint64_t t[2];
        # uint64_t f[2];
        for ($i = 1; $i < 3; ++$i) {
            /** @var array<int, ParagonIE_Sodium_Core32_Int64> $ctxA */
            $ctxA = $ctx[$i]->toArray();
            /** @var ParagonIE_Sodium_Core32_Int64 $ctxA1 */
            $ctxA1 = $ctxA[0];
            /** @var ParagonIE_Sodium_Core32_Int64 $ctxA2 */
            $ctxA2 = $ctxA[1];

            $str .= $ctxA1->toReverseString();
            $str .= $ctxA2->toReverseString();
        }

        # uint8_t buf[2 * 128];
        $str .= self::SplFixedArrayToString($ctx[3]);

        /** @var int $ctx4 */
        $ctx4 = $ctx[4];

        # size_t buflen;
        $str .= implode('', array(
            self::intToChr($ctx4 & 0xff),
            self::intToChr(($ctx4 >> 8) & 0xff),
            self::intToChr(($ctx4 >> 16) & 0xff),
            self::intToChr(($ctx4 >> 24) & 0xff),
            "\x00\x00\x00\x00"
            /*
            self::intToChr(($ctx4 >> 32) & 0xff),
            self::intToChr(($ctx4 >> 40) & 0xff),
            self::intToChr(($ctx4 >> 48) & 0xff),
            self::intToChr(($ctx4 >> 56) & 0xff)
            */
        ));
        # uint8_t last_node;
        return $str . self::intToChr($ctx[5]) . str_repeat("\x00", 23);
    }

    /**
     * Creates an SplFixedArray containing other SplFixedArray elements, from
     * a string (compatible with \Sodium\crypto_generichash_{init, update, final})
     *
     * @internal You should not use this directly from another application
     *
     * @param string $string
     * @return SplFixedArray
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArrayAccess
     * @psalm-suppress MixedArrayAssignment
     */
    public static function stringToContext($string)
    {
        $ctx = self::context();

        # uint64_t h[8];
        for ($i = 0; $i < 8; ++$i) {
            $ctx[0][$i] = ParagonIE_Sodium_Core32_Int64::fromReverseString(
                self::substr($string, (($i << 3) + 0), 8)
            );
        }

        # uint64_t t[2];
        # uint64_t f[2];
        for ($i = 1; $i < 3; ++$i) {
            $ctx[$i][1] = ParagonIE_Sodium_Core32_Int64::fromReverseString(
                self::substr($string, 72 + (($i - 1) << 4), 8)
            );
            $ctx[$i][0] = ParagonIE_Sodium_Core32_Int64::fromReverseString(
                self::substr($string, 64 + (($i - 1) << 4), 8)
            );
        }

        # uint8_t buf[2 * 128];
        $ctx[3] = self::stringToSplFixedArray(self::substr($string, 96, 256));

        # uint8_t buf[2 * 128];
        $int = 0;
        for ($i = 0; $i < 8; ++$i) {
            $int |= self::chrToInt($string[352 + $i]) << ($i << 3);
        }
        $ctx[4] = $int;

        return $ctx;
    }
}
Core32/Poly1305.php000064400000003062152331075510007551 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_Poly1305', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_Poly1305
 */
abstract class ParagonIE_Sodium_Core32_Poly1305 extends ParagonIE_Sodium_Core32_Util
{
    const BLOCK_SIZE = 16;

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $m
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function onetimeauth($m, $key)
    {
        if (self::strlen($key) < 32) {
            throw new InvalidArgumentException(
                'Key must be 32 bytes long.'
            );
        }
        $state = new ParagonIE_Sodium_Core32_Poly1305_State(
            self::substr($key, 0, 32)
        );
        return $state
            ->update($m)
            ->finish();
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $mac
     * @param string $m
     * @param string $key
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function onetimeauth_verify($mac, $m, $key)
    {
        if (self::strlen($key) < 32) {
            throw new InvalidArgumentException(
                'Key must be 32 bytes long.'
            );
        }
        $state = new ParagonIE_Sodium_Core32_Poly1305_State(
            self::substr($key, 0, 32)
        );
        $calc = $state
            ->update($m)
            ->finish();
        return self::verify_16($calc, $mac);
    }
}
Core32/Salsa20.php000064400000026362152331075510007532 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_Salsa20', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_Salsa20
 */
abstract class ParagonIE_Sodium_Core32_Salsa20 extends ParagonIE_Sodium_Core32_Util
{
    const ROUNDS = 20;

    /**
     * Calculate an salsa20 hash of a single block
     *
     * @internal You should not use this directly from another application
     *
     * @param string $in
     * @param string $k
     * @param string|null $c
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function core_salsa20($in, $k, $c = null)
    {
        /**
         * @var ParagonIE_Sodium_Core32_Int32 $x0
         * @var ParagonIE_Sodium_Core32_Int32 $x1
         * @var ParagonIE_Sodium_Core32_Int32 $x2
         * @var ParagonIE_Sodium_Core32_Int32 $x3
         * @var ParagonIE_Sodium_Core32_Int32 $x4
         * @var ParagonIE_Sodium_Core32_Int32 $x5
         * @var ParagonIE_Sodium_Core32_Int32 $x6
         * @var ParagonIE_Sodium_Core32_Int32 $x7
         * @var ParagonIE_Sodium_Core32_Int32 $x8
         * @var ParagonIE_Sodium_Core32_Int32 $x9
         * @var ParagonIE_Sodium_Core32_Int32 $x10
         * @var ParagonIE_Sodium_Core32_Int32 $x11
         * @var ParagonIE_Sodium_Core32_Int32 $x12
         * @var ParagonIE_Sodium_Core32_Int32 $x13
         * @var ParagonIE_Sodium_Core32_Int32 $x14
         * @var ParagonIE_Sodium_Core32_Int32 $x15
         * @var ParagonIE_Sodium_Core32_Int32 $j0
         * @var ParagonIE_Sodium_Core32_Int32 $j1
         * @var ParagonIE_Sodium_Core32_Int32 $j2
         * @var ParagonIE_Sodium_Core32_Int32 $j3
         * @var ParagonIE_Sodium_Core32_Int32 $j4
         * @var ParagonIE_Sodium_Core32_Int32 $j5
         * @var ParagonIE_Sodium_Core32_Int32 $j6
         * @var ParagonIE_Sodium_Core32_Int32 $j7
         * @var ParagonIE_Sodium_Core32_Int32 $j8
         * @var ParagonIE_Sodium_Core32_Int32 $j9
         * @var ParagonIE_Sodium_Core32_Int32 $j10
         * @var ParagonIE_Sodium_Core32_Int32 $j11
         * @var ParagonIE_Sodium_Core32_Int32 $j12
         * @var ParagonIE_Sodium_Core32_Int32 $j13
         * @var ParagonIE_Sodium_Core32_Int32 $j14
         * @var ParagonIE_Sodium_Core32_Int32 $j15
         */
        if (self::strlen($k) < 32) {
            throw new RangeException('Key must be 32 bytes long');
        }
        if ($c === null) {
            $x0  = new ParagonIE_Sodium_Core32_Int32(array(0x6170, 0x7865));
            $x5  = new ParagonIE_Sodium_Core32_Int32(array(0x3320, 0x646e));
            $x10 = new ParagonIE_Sodium_Core32_Int32(array(0x7962, 0x2d32));
            $x15 = new ParagonIE_Sodium_Core32_Int32(array(0x6b20, 0x6574));
        } else {
            $x0  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($c, 0, 4));
            $x5  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($c, 4, 4));
            $x10 = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($c, 8, 4));
            $x15 = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($c, 12, 4));
        }
        $x1  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 0, 4));
        $x2  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 4, 4));
        $x3  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 8, 4));
        $x4  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 12, 4));
        $x6  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($in, 0, 4));
        $x7  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($in, 4, 4));
        $x8  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($in, 8, 4));
        $x9  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($in, 12, 4));
        $x11 = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 16, 4));
        $x12 = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 20, 4));
        $x13 = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 24, 4));
        $x14 = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($k, 28, 4));

        $j0  = clone $x0;
        $j1  = clone $x1;
        $j2  = clone $x2;
        $j3  = clone $x3;
        $j4  = clone $x4;
        $j5  = clone $x5;
        $j6  = clone $x6;
        $j7  = clone $x7;
        $j8  = clone $x8;
        $j9  = clone $x9;
        $j10  = clone $x10;
        $j11  = clone $x11;
        $j12  = clone $x12;
        $j13  = clone $x13;
        $j14  = clone $x14;
        $j15  = clone $x15;

        for ($i = self::ROUNDS; $i > 0; $i -= 2) {
            $x4  = $x4->xorInt32($x0->addInt32($x12)->rotateLeft(7));
            $x8  = $x8->xorInt32($x4->addInt32($x0)->rotateLeft(9));
            $x12 = $x12->xorInt32($x8->addInt32($x4)->rotateLeft(13));
            $x0  = $x0->xorInt32($x12->addInt32($x8)->rotateLeft(18));

            $x9  = $x9->xorInt32($x5->addInt32($x1)->rotateLeft(7));
            $x13 = $x13->xorInt32($x9->addInt32($x5)->rotateLeft(9));
            $x1  = $x1->xorInt32($x13->addInt32($x9)->rotateLeft(13));
            $x5  = $x5->xorInt32($x1->addInt32($x13)->rotateLeft(18));

            $x14 = $x14->xorInt32($x10->addInt32($x6)->rotateLeft(7));
            $x2  = $x2->xorInt32($x14->addInt32($x10)->rotateLeft(9));
            $x6  = $x6->xorInt32($x2->addInt32($x14)->rotateLeft(13));
            $x10 = $x10->xorInt32($x6->addInt32($x2)->rotateLeft(18));

            $x3  = $x3->xorInt32($x15->addInt32($x11)->rotateLeft(7));
            $x7  = $x7->xorInt32($x3->addInt32($x15)->rotateLeft(9));
            $x11 = $x11->xorInt32($x7->addInt32($x3)->rotateLeft(13));
            $x15 = $x15->xorInt32($x11->addInt32($x7)->rotateLeft(18));

            $x1  = $x1->xorInt32($x0->addInt32($x3)->rotateLeft(7));
            $x2  = $x2->xorInt32($x1->addInt32($x0)->rotateLeft(9));
            $x3  = $x3->xorInt32($x2->addInt32($x1)->rotateLeft(13));
            $x0  = $x0->xorInt32($x3->addInt32($x2)->rotateLeft(18));

            $x6  = $x6->xorInt32($x5->addInt32($x4)->rotateLeft(7));
            $x7  = $x7->xorInt32($x6->addInt32($x5)->rotateLeft(9));
            $x4  = $x4->xorInt32($x7->addInt32($x6)->rotateLeft(13));
            $x5  = $x5->xorInt32($x4->addInt32($x7)->rotateLeft(18));

            $x11 = $x11->xorInt32($x10->addInt32($x9)->rotateLeft(7));
            $x8  = $x8->xorInt32($x11->addInt32($x10)->rotateLeft(9));
            $x9  = $x9->xorInt32($x8->addInt32($x11)->rotateLeft(13));
            $x10 = $x10->xorInt32($x9->addInt32($x8)->rotateLeft(18));

            $x12 = $x12->xorInt32($x15->addInt32($x14)->rotateLeft(7));
            $x13 = $x13->xorInt32($x12->addInt32($x15)->rotateLeft(9));
            $x14 = $x14->xorInt32($x13->addInt32($x12)->rotateLeft(13));
            $x15 = $x15->xorInt32($x14->addInt32($x13)->rotateLeft(18));
        }

        $x0  = $x0->addInt32($j0);
        $x1  = $x1->addInt32($j1);
        $x2  = $x2->addInt32($j2);
        $x3  = $x3->addInt32($j3);
        $x4  = $x4->addInt32($j4);
        $x5  = $x5->addInt32($j5);
        $x6  = $x6->addInt32($j6);
        $x7  = $x7->addInt32($j7);
        $x8  = $x8->addInt32($j8);
        $x9  = $x9->addInt32($j9);
        $x10 = $x10->addInt32($j10);
        $x11 = $x11->addInt32($j11);
        $x12 = $x12->addInt32($j12);
        $x13 = $x13->addInt32($j13);
        $x14 = $x14->addInt32($j14);
        $x15 = $x15->addInt32($j15);

        return $x0->toReverseString() .
            $x1->toReverseString() .
            $x2->toReverseString() .
            $x3->toReverseString() .
            $x4->toReverseString() .
            $x5->toReverseString() .
            $x6->toReverseString() .
            $x7->toReverseString() .
            $x8->toReverseString() .
            $x9->toReverseString() .
            $x10->toReverseString() .
            $x11->toReverseString() .
            $x12->toReverseString() .
            $x13->toReverseString() .
            $x14->toReverseString() .
            $x15->toReverseString();
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $len
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function salsa20($len, $nonce, $key)
    {
        if (self::strlen($key) !== 32) {
            throw new RangeException('Key must be 32 bytes long');
        }
        $kcopy = '' . $key;
        $in = self::substr($nonce, 0, 8) . str_repeat("\0", 8);
        $c = '';
        while ($len >= 64) {
            $c .= self::core_salsa20($in, $kcopy, null);
            $u = 1;
            // Internal counter.
            for ($i = 8; $i < 16; ++$i) {
                $u += self::chrToInt($in[$i]);
                $in[$i] = self::intToChr($u & 0xff);
                $u >>= 8;
            }
            $len -= 64;
        }
        if ($len > 0) {
            $c .= self::substr(
                self::core_salsa20($in, $kcopy, null),
                0,
                $len
            );
        }
        try {
            ParagonIE_Sodium_Compat::memzero($kcopy);
        } catch (SodiumException $ex) {
            $kcopy = null;
        }
        return $c;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $m
     * @param string $n
     * @param int $ic
     * @param string $k
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function salsa20_xor_ic($m, $n, $ic, $k)
    {
        $mlen = self::strlen($m);
        if ($mlen < 1) {
            return '';
        }
        $kcopy = self::substr($k, 0, 32);
        $in = self::substr($n, 0, 8);
        // Initialize the counter
        $in .= ParagonIE_Sodium_Core32_Util::store64_le($ic);

        $c = '';
        while ($mlen >= 64) {
            $block = self::core_salsa20($in, $kcopy, null);
            $c .= self::xorStrings(
                self::substr($m, 0, 64),
                self::substr($block, 0, 64)
            );
            $u = 1;
            for ($i = 8; $i < 16; ++$i) {
                $u += self::chrToInt($in[$i]);
                $in[$i] = self::intToChr($u & 0xff);
                $u >>= 8;
            }

            $mlen -= 64;
            $m = self::substr($m, 64);
        }

        if ($mlen) {
            $block = self::core_salsa20($in, $kcopy, null);
            $c .= self::xorStrings(
                self::substr($m, 0, $mlen),
                self::substr($block, 0, $mlen)
            );
        }
        try {
            ParagonIE_Sodium_Compat::memzero($block);
            ParagonIE_Sodium_Compat::memzero($kcopy);
        } catch (SodiumException $ex) {
            $block = null;
            $kcopy = null;
        }

        return $c;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function salsa20_xor($message, $nonce, $key)
    {
        return self::xorStrings(
            $message,
            self::salsa20(
                self::strlen($message),
                $nonce,
                $key
            )
        );
    }
}
Core32/XSalsa20.php000064400000002543152331075510007655 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_XSalsa20', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_XSalsa20
 */
abstract class ParagonIE_Sodium_Core32_XSalsa20 extends ParagonIE_Sodium_Core32_HSalsa20
{
    /**
     * Expand a key and nonce into an xsalsa20 keystream.
     *
     * @internal You should not use this directly from another application
     *
     * @param int $len
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function xsalsa20($len, $nonce, $key)
    {
        $ret = self::salsa20(
            $len,
            self::substr($nonce, 16, 8),
            self::hsalsa20($nonce, $key)
        );
        return $ret;
    }

    /**
     * Encrypt a string with XSalsa20. Doesn't provide integrity.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $message
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function xsalsa20_xor($message, $nonce, $key)
    {
        return self::xorStrings(
            $message,
            self::xsalsa20(
                self::strlen($message),
                $nonce,
                $key
            )
        );
    }
}
Core32/Int32.php000064400000057736152331075510007235 0ustar00<?php

/**
 * Class ParagonIE_Sodium_Core32_Int32
 *
 * Encapsulates a 32-bit integer.
 *
 * These are immutable. It always returns a new instance.
 */
class ParagonIE_Sodium_Core32_Int32
{
    /**
     * @var array<int, int> - two 16-bit integers
     *
     * 0 is the higher 16 bits
     * 1 is the lower 16 bits
     */
    public $limbs = array(0, 0);

    /**
     * @var int
     */
    public $overflow = 0;

    /**
     * @var bool
     */
    public $unsignedInt = false;

    /**
     * ParagonIE_Sodium_Core32_Int32 constructor.
     * @param array $array
     * @param bool $unsignedInt
     */
    public function __construct($array = array(0, 0), $unsignedInt = false)
    {
        $this->limbs = array(
            (int) $array[0],
            (int) $array[1]
        );
        $this->overflow = 0;
        $this->unsignedInt = $unsignedInt;
    }

    /**
     * Adds two int32 objects
     *
     * @param ParagonIE_Sodium_Core32_Int32 $addend
     * @return ParagonIE_Sodium_Core32_Int32
     */
    public function addInt32(ParagonIE_Sodium_Core32_Int32 $addend)
    {
        $i0 = $this->limbs[0];
        $i1 = $this->limbs[1];
        $j0 = $addend->limbs[0];
        $j1 = $addend->limbs[1];

        $r1 = $i1 + ($j1 & 0xffff);
        $carry = $r1 >> 16;

        $r0 = $i0 + ($j0 & 0xffff) + $carry;
        $carry = $r0 >> 16;

        $r0 &= 0xffff;
        $r1 &= 0xffff;

        $return = new ParagonIE_Sodium_Core32_Int32(
            array($r0, $r1)
        );
        $return->overflow = $carry;
        $return->unsignedInt = $this->unsignedInt;
        return $return;
    }

    /**
     * Adds a normal integer to an int32 object
     *
     * @param int $int
     * @return ParagonIE_Sodium_Core32_Int32
     * @throws SodiumException
     * @throws TypeError
     */
    public function addInt($int)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($int, 'int', 1);
        /** @var int $int */
        $int = (int) $int;

        $int = (int) $int;

        $i0 = $this->limbs[0];
        $i1 = $this->limbs[1];

        $r1 = $i1 + ($int & 0xffff);
        $carry = $r1 >> 16;

        $r0 = $i0 + (($int >> 16) & 0xffff) + $carry;
        $carry = $r0 >> 16;
        $r0 &= 0xffff;
        $r1 &= 0xffff;
        $return = new ParagonIE_Sodium_Core32_Int32(
            array($r0, $r1)
        );
        $return->overflow = $carry;
        $return->unsignedInt = $this->unsignedInt;
        return $return;
    }

    /**
     * @param int $b
     * @return int
     */
    public function compareInt($b = 0)
    {
        $gt = 0;
        $eq = 1;

        $i = 2;
        $j = 0;
        while ($i > 0) {
            --$i;
            /** @var int $x1 */
            $x1 = $this->limbs[$i];
            /** @var int $x2 */
            $x2 = ($b >> ($j << 4)) & 0xffff;
            /** @var int $gt */
            $gt |= (($x2 - $x1) >> 8) & $eq;
            /** @var int $eq */
            $eq &= (($x2 ^ $x1) - 1) >> 8;
        }
        return ($gt + $gt - $eq) + 1;
    }

    /**
     * @param int $m
     * @return ParagonIE_Sodium_Core32_Int32
     */
    public function mask($m = 0)
    {
        /** @var int $hi */
        $hi = ($m >> 16) & 0xffff;
        /** @var int $lo */
        $lo = ($m & 0xffff);
        return new ParagonIE_Sodium_Core32_Int32(
            array(
                (int) ($this->limbs[0] & $hi),
                (int) ($this->limbs[1] & $lo)
            ),
            $this->unsignedInt
        );
    }

    /**
     * @param array<int, int> $a
     * @param array<int, int> $b
     * @param int $baseLog2
     * @return array<int, int>
     */
    public function multiplyLong(array $a, array $b, $baseLog2 = 16)
    {
        $a_l = count($a);
        $b_l = count($b);
        /** @var array<int, int> $r */
        $r = array_fill(0, $a_l + $b_l + 1, 0);
        $base = 1 << $baseLog2;
        for ($i = 0; $i < $a_l; ++$i) {
            $a_i = $a[$i];
            for ($j = 0; $j < $a_l; ++$j) {
                $b_j = $b[$j];
                $product = ($a_i * $b_j) + $r[$i + $j];
                $carry = ($product >> $baseLog2 & 0xffff);
                $r[$i + $j] = ($product - (int) ($carry * $base)) & 0xffff;
                $r[$i + $j + 1] += $carry;
            }
        }
        return array_slice($r, 0, 5);
    }

    /**
     * @param int $int
     * @return ParagonIE_Sodium_Core32_Int32
     */
    public function mulIntFast($int)
    {
        // Handle negative numbers
        $aNeg = ($this->limbs[0] >> 15) & 1;
        $bNeg = ($int >> 31) & 1;
        $a = array_reverse($this->limbs);
        $b = array(
            $int & 0xffff,
            ($int >> 16) & 0xffff
        );
        if ($aNeg) {
            for ($i = 0; $i < 2; ++$i) {
                $a[$i] = ($a[$i] ^ 0xffff) & 0xffff;
            }
            ++$a[0];
        }
        if ($bNeg) {
            for ($i = 0; $i < 2; ++$i) {
                $b[$i] = ($b[$i] ^ 0xffff) & 0xffff;
            }
            ++$b[0];
        }
        // Multiply
        $res = $this->multiplyLong($a, $b);

        // Re-apply negation to results
        if ($aNeg !== $bNeg) {
            for ($i = 0; $i < 2; ++$i) {
                $res[$i] = (0xffff ^ $res[$i]) & 0xffff;
            }
            // Handle integer overflow
            $c = 1;
            for ($i = 0; $i < 2; ++$i) {
                $res[$i] += $c;
                $c = $res[$i] >> 16;
                $res[$i] &= 0xffff;
            }
        }

        // Return our values
        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->limbs = array(
            $res[1] & 0xffff,
            $res[0] & 0xffff
        );
        if (count($res) > 2) {
            $return->overflow = $res[2] & 0xffff;
        }
        $return->unsignedInt = $this->unsignedInt;
        return $return;
    }

    /**
     * @param ParagonIE_Sodium_Core32_Int32 $right
     * @return ParagonIE_Sodium_Core32_Int32
     */
    public function mulInt32Fast(ParagonIE_Sodium_Core32_Int32 $right)
    {
        $aNeg = ($this->limbs[0] >> 15) & 1;
        $bNeg = ($right->limbs[0] >> 15) & 1;

        $a = array_reverse($this->limbs);
        $b = array_reverse($right->limbs);
        if ($aNeg) {
            for ($i = 0; $i < 2; ++$i) {
                $a[$i] = ($a[$i] ^ 0xffff) & 0xffff;
            }
            ++$a[0];
        }
        if ($bNeg) {
            for ($i = 0; $i < 2; ++$i) {
                $b[$i] = ($b[$i] ^ 0xffff) & 0xffff;
            }
            ++$b[0];
        }
        $res = $this->multiplyLong($a, $b);
        if ($aNeg !== $bNeg) {
            if ($aNeg !== $bNeg) {
                for ($i = 0; $i < 2; ++$i) {
                    $res[$i] = ($res[$i] ^ 0xffff) & 0xffff;
                }
                $c = 1;
                for ($i = 0; $i < 2; ++$i) {
                    $res[$i] += $c;
                    $c = $res[$i] >> 16;
                    $res[$i] &= 0xffff;
                }
            }
        }
        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->limbs = array(
            $res[1] & 0xffff,
            $res[0] & 0xffff
        );
        if (count($res) > 2) {
            $return->overflow = $res[2];
        }
        return $return;
    }

    /**
     * @param int $int
     * @param int $size
     * @return ParagonIE_Sodium_Core32_Int32
     * @throws SodiumException
     * @throws TypeError
     */
    public function mulInt($int = 0, $size = 0)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($int, 'int', 1);
        ParagonIE_Sodium_Core32_Util::declareScalarType($size, 'int', 2);
        if (ParagonIE_Sodium_Compat::$fastMult) {
            return $this->mulIntFast((int) $int);
        }
        /** @var int $int */
        $int = (int) $int;
        /** @var int $size */
        $size = (int) $size;

        if (!$size) {
            $size = 31;
        }
        /** @var int $size */

        $a = clone $this;
        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->unsignedInt = $this->unsignedInt;

        // Initialize:
        $ret0 = 0;
        $ret1 = 0;
        $a0 = $a->limbs[0];
        $a1 = $a->limbs[1];

        /** @var int $size */
        /** @var int $i */
        for ($i = $size; $i >= 0; --$i) {
            $m = (int) (-($int & 1));
            $x0 = $a0 & $m;
            $x1 = $a1 & $m;

            $ret1 += $x1;
            $c = $ret1 >> 16;

            $ret0 += $x0 + $c;

            $ret0 &= 0xffff;
            $ret1 &= 0xffff;

            $a1 = ($a1 << 1);
            $x1 = $a1 >> 16;
            $a0 = ($a0 << 1) | $x1;
            $a0 &= 0xffff;
            $a1 &= 0xffff;
            $int >>= 1;
        }
        $return->limbs[0] = $ret0;
        $return->limbs[1] = $ret1;
        return $return;
    }

    /**
     * @param ParagonIE_Sodium_Core32_Int32 $int
     * @param int $size
     * @return ParagonIE_Sodium_Core32_Int32
     * @throws SodiumException
     * @throws TypeError
     */
    public function mulInt32(ParagonIE_Sodium_Core32_Int32 $int, $size = 0)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($size, 'int', 2);
        if (ParagonIE_Sodium_Compat::$fastMult) {
            return $this->mulInt32Fast($int);
        }
        if (!$size) {
            $size = 31;
        }
        /** @var int $size */

        $a = clone $this;
        $b = clone $int;
        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->unsignedInt = $this->unsignedInt;

        // Initialize:
        $ret0 = 0;
        $ret1 = 0;
        $a0 = $a->limbs[0];
        $a1 = $a->limbs[1];
        $b0 = $b->limbs[0];
        $b1 = $b->limbs[1];

        /** @var int $size */
        /** @var int $i */
        for ($i = $size; $i >= 0; --$i) {
            $m = (int) (-($b1 & 1));
            $x0 = $a0 & $m;
            $x1 = $a1 & $m;

            $ret1 += $x1;
            $c = $ret1 >> 16;

            $ret0 += $x0 + $c;

            $ret0 &= 0xffff;
            $ret1 &= 0xffff;

            $a1 = ($a1 << 1);
            $x1 = $a1 >> 16;
            $a0 = ($a0 << 1) | $x1;
            $a0 &= 0xffff;
            $a1 &= 0xffff;

            $x0 = ($b0 & 1) << 16;
            $b0 = ($b0 >> 1);
            $b1 = (($b1 | $x0) >> 1);

            $b0 &= 0xffff;
            $b1 &= 0xffff;

        }
        $return->limbs[0] = $ret0;
        $return->limbs[1] = $ret1;

        return $return;
    }

    /**
     * OR this 32-bit integer with another.
     *
     * @param ParagonIE_Sodium_Core32_Int32 $b
     * @return ParagonIE_Sodium_Core32_Int32
     */
    public function orInt32(ParagonIE_Sodium_Core32_Int32 $b)
    {
        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->unsignedInt = $this->unsignedInt;
        $return->limbs = array(
            (int) ($this->limbs[0] | $b->limbs[0]),
            (int) ($this->limbs[1] | $b->limbs[1])
        );
        /** @var int overflow */
        $return->overflow = $this->overflow | $b->overflow;
        return $return;
    }

    /**
     * @param int $b
     * @return bool
     */
    public function isGreaterThan($b = 0)
    {
        return $this->compareInt($b) > 0;
    }

    /**
     * @param int $b
     * @return bool
     */
    public function isLessThanInt($b = 0)
    {
        return $this->compareInt($b) < 0;
    }

    /**
     * @param int $c
     * @return ParagonIE_Sodium_Core32_Int32
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArrayAccess
     */
    public function rotateLeft($c = 0)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($c, 'int', 1);
        /** @var int $c */
        $c = (int) $c;

        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->unsignedInt = $this->unsignedInt;
        $c &= 31;
        if ($c === 0) {
            // NOP, but we want a copy.
            $return->limbs = $this->limbs;
        } else {
            /** @var int $c */

            /** @var int $idx_shift */
            $idx_shift = ($c >> 4) & 1;

            /** @var int $sub_shift */
            $sub_shift = $c & 15;

            /** @var array<int, int> $limbs */
            $limbs =& $return->limbs;

            /** @var array<int, int> $myLimbs */
            $myLimbs =& $this->limbs;

            for ($i = 1; $i >= 0; --$i) {
                /** @var int $j */
                $j = ($i + $idx_shift) & 1;
                /** @var int $k */
                $k = ($i + $idx_shift + 1) & 1;
                $limbs[$i] = (int) (
                    (
                        ((int) ($myLimbs[$j]) << $sub_shift)
                            |
                        ((int) ($myLimbs[$k]) >> (16 - $sub_shift))
                    ) & 0xffff
                );
            }
        }
        return $return;
    }

    /**
     * Rotate to the right
     *
     * @param int $c
     * @return ParagonIE_Sodium_Core32_Int32
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArrayAccess
     */
    public function rotateRight($c = 0)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($c, 'int', 1);
        /** @var int $c */
        $c = (int) $c;

        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->unsignedInt = $this->unsignedInt;
        $c &= 31;
        /** @var int $c */
        if ($c === 0) {
            // NOP, but we want a copy.
            $return->limbs = $this->limbs;
        } else {
            /** @var int $c */

            /** @var int $idx_shift */
            $idx_shift = ($c >> 4) & 1;

            /** @var int $sub_shift */
            $sub_shift = $c & 15;

            /** @var array<int, int> $limbs */
            $limbs =& $return->limbs;

            /** @var array<int, int> $myLimbs */
            $myLimbs =& $this->limbs;

            for ($i = 1; $i >= 0; --$i) {
                /** @var int $j */
                $j = ($i - $idx_shift) & 1;
                /** @var int $k */
                $k = ($i - $idx_shift - 1) & 1;
                $limbs[$i] = (int) (
                    (
                        ((int) ($myLimbs[$j]) >> (int) ($sub_shift))
                            |
                        ((int) ($myLimbs[$k]) << (16 - (int) ($sub_shift)))
                    ) & 0xffff
                );
            }
        }
        return $return;
    }

    /**
     * @param bool $bool
     * @return self
     */
    public function setUnsignedInt($bool = false)
    {
        $this->unsignedInt = !empty($bool);
        return $this;
    }

    /**
     * @param int $c
     * @return ParagonIE_Sodium_Core32_Int32
     * @throws SodiumException
     * @throws TypeError
     */
    public function shiftLeft($c = 0)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($c, 'int', 1);
        /** @var int $c */
        $c = (int) $c;

        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->unsignedInt = $this->unsignedInt;
        $c &= 63;
        /** @var int $c */
        if ($c === 0) {
            $return->limbs = $this->limbs;
        } elseif ($c < 0) {
            /** @var int $c */
            return $this->shiftRight(-$c);
        } else {
            /** @var int $c */
            /** @var int $tmp */
            $tmp = $this->limbs[1] << $c;
            $return->limbs[1] = (int)($tmp & 0xffff);
            /** @var int $carry */
            $carry = $tmp >> 16;

            /** @var int $tmp */
            $tmp = ($this->limbs[0] << $c) | ($carry & 0xffff);
            $return->limbs[0] = (int) ($tmp & 0xffff);
        }
        return $return;
    }

    /**
     * @param int $c
     * @return ParagonIE_Sodium_Core32_Int32
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedAssignment
     * @psalm-suppress MixedOperand
     */
    public function shiftRight($c = 0)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($c, 'int', 1);
        /** @var int $c */
        $c = (int) $c;

        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->unsignedInt = $this->unsignedInt;
        $c &= 63;
        /** @var int $c */
        if ($c >= 16) {
            $return->limbs = array(
                (int) ($this->overflow & 0xffff),
                (int) ($this->limbs[0])
            );
            $return->overflow = $this->overflow >> 16;
            return $return->shiftRight($c & 15);
        }
        if ($c === 0) {
            $return->limbs = $this->limbs;
        } elseif ($c < 0) {
            /** @var int $c */
            return $this->shiftLeft(-$c);
        } else {
            if (!is_int($c)) {
                throw new TypeError();
            }
            /** @var int $c */
            // $return->limbs[0] = (int) (($this->limbs[0] >> $c) & 0xffff);
            $carryLeft = (int) ($this->overflow & ((1 << ($c + 1)) - 1));
            $return->limbs[0] = (int) ((($this->limbs[0] >> $c) | ($carryLeft << (16 - $c))) & 0xffff);
            $carryRight = (int) ($this->limbs[0] & ((1 << ($c + 1)) - 1));
            $return->limbs[1] = (int) ((($this->limbs[1] >> $c) | ($carryRight << (16 - $c))) & 0xffff);
            $return->overflow >>= $c;
        }
        return $return;
    }

    /**
     * Subtract a normal integer from an int32 object.
     *
     * @param int $int
     * @return ParagonIE_Sodium_Core32_Int32
     * @throws SodiumException
     * @throws TypeError
     */
    public function subInt($int)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($int, 'int', 1);
        /** @var int $int */
        $int = (int) $int;

        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->unsignedInt = $this->unsignedInt;

        /** @var int $tmp */
        $tmp = $this->limbs[1] - ($int & 0xffff);
        /** @var int $carry */
        $carry = $tmp >> 16;
        $return->limbs[1] = (int) ($tmp & 0xffff);

        /** @var int $tmp */
        $tmp = $this->limbs[0] - (($int >> 16) & 0xffff) + $carry;
        $return->limbs[0] = (int) ($tmp & 0xffff);
        return $return;
    }

    /**
     * Subtract two int32 objects from each other
     *
     * @param ParagonIE_Sodium_Core32_Int32 $b
     * @return ParagonIE_Sodium_Core32_Int32
     */
    public function subInt32(ParagonIE_Sodium_Core32_Int32 $b)
    {
        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->unsignedInt = $this->unsignedInt;

        /** @var int $tmp */
        $tmp = $this->limbs[1] - ($b->limbs[1] & 0xffff);
        /** @var int $carry */
        $carry = $tmp >> 16;
        $return->limbs[1] = (int) ($tmp & 0xffff);

        /** @var int $tmp */
        $tmp = $this->limbs[0] - ($b->limbs[0] & 0xffff) + $carry;
        $return->limbs[0] = (int) ($tmp & 0xffff);
        return $return;
    }

    /**
     * XOR this 32-bit integer with another.
     *
     * @param ParagonIE_Sodium_Core32_Int32 $b
     * @return ParagonIE_Sodium_Core32_Int32
     */
    public function xorInt32(ParagonIE_Sodium_Core32_Int32 $b)
    {
        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->unsignedInt = $this->unsignedInt;
        $return->limbs = array(
            (int) ($this->limbs[0] ^ $b->limbs[0]),
            (int) ($this->limbs[1] ^ $b->limbs[1])
        );
        return $return;
    }

    /**
     * @param int $signed
     * @return self
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fromInt($signed)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($signed, 'int', 1);;
        /** @var int $signed */
        $signed = (int) $signed;

        return new ParagonIE_Sodium_Core32_Int32(
            array(
                (int) (($signed >> 16) & 0xffff),
                (int) ($signed & 0xffff)
            )
        );
    }

    /**
     * @param string $string
     * @return self
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fromString($string)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($string, 'string', 1);
        $string = (string) $string;
        if (ParagonIE_Sodium_Core32_Util::strlen($string) !== 4) {
            throw new RangeException(
                'String must be 4 bytes; ' . ParagonIE_Sodium_Core32_Util::strlen($string) . ' given.'
            );
        }
        $return = new ParagonIE_Sodium_Core32_Int32();

        $return->limbs[0]  = (int) ((ParagonIE_Sodium_Core32_Util::chrToInt($string[0]) & 0xff) << 8);
        $return->limbs[0] |= (ParagonIE_Sodium_Core32_Util::chrToInt($string[1]) & 0xff);
        $return->limbs[1]  = (int) ((ParagonIE_Sodium_Core32_Util::chrToInt($string[2]) & 0xff) << 8);
        $return->limbs[1] |= (ParagonIE_Sodium_Core32_Util::chrToInt($string[3]) & 0xff);
        return $return;
    }

    /**
     * @param string $string
     * @return self
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fromReverseString($string)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($string, 'string', 1);
        $string = (string) $string;
        if (ParagonIE_Sodium_Core32_Util::strlen($string) !== 4) {
            throw new RangeException(
                'String must be 4 bytes; ' . ParagonIE_Sodium_Core32_Util::strlen($string) . ' given.'
            );
        }
        $return = new ParagonIE_Sodium_Core32_Int32();

        $return->limbs[0]  = (int) ((ParagonIE_Sodium_Core32_Util::chrToInt($string[3]) & 0xff) << 8);
        $return->limbs[0] |= (ParagonIE_Sodium_Core32_Util::chrToInt($string[2]) & 0xff);
        $return->limbs[1]  = (int) ((ParagonIE_Sodium_Core32_Util::chrToInt($string[1]) & 0xff) << 8);
        $return->limbs[1] |= (ParagonIE_Sodium_Core32_Util::chrToInt($string[0]) & 0xff);
        return $return;
    }

    /**
     * @return array<int, int>
     */
    public function toArray()
    {
        return array((int) ($this->limbs[0] << 16 | $this->limbs[1]));
    }

    /**
     * @return string
     * @throws TypeError
     */
    public function toString()
    {
        return
            ParagonIE_Sodium_Core32_Util::intToChr(($this->limbs[0] >> 8) & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr($this->limbs[0] & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr(($this->limbs[1] >> 8) & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr($this->limbs[1] & 0xff);
    }

    /**
     * @return int
     */
    public function toInt()
    {
        return (int) (
            (($this->limbs[0] & 0xffff) << 16)
                |
            ($this->limbs[1] & 0xffff)
        );
    }

    /**
     * @return ParagonIE_Sodium_Core32_Int32
     */
    public function toInt32()
    {
        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->limbs[0] = (int) ($this->limbs[0] & 0xffff);
        $return->limbs[1] = (int) ($this->limbs[1] & 0xffff);
        $return->unsignedInt = $this->unsignedInt;
        $return->overflow = (int) ($this->overflow & 0x7fffffff);
        return $return;
    }

    /**
     * @return ParagonIE_Sodium_Core32_Int64
     */
    public function toInt64()
    {
        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->unsignedInt = $this->unsignedInt;
        if ($this->unsignedInt) {
            $return->limbs[0] += (($this->overflow >> 16) & 0xffff);
            $return->limbs[1] += (($this->overflow) & 0xffff);
        } else {
            $neg = -(($this->limbs[0] >> 15) & 1);
            $return->limbs[0] = (int)($neg & 0xffff);
            $return->limbs[1] = (int)($neg & 0xffff);
        }
        $return->limbs[2] = (int) ($this->limbs[0] & 0xffff);
        $return->limbs[3] = (int) ($this->limbs[1] & 0xffff);
        return $return;
    }

    /**
     * @return string
     * @throws TypeError
     */
    public function toReverseString()
    {
        return ParagonIE_Sodium_Core32_Util::intToChr($this->limbs[1] & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr(($this->limbs[1] >> 8) & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr($this->limbs[0] & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr(($this->limbs[0] >> 8) & 0xff);
    }

    /**
     * @return string
     */
    public function __toString()
    {
        try {
            return $this->toString();
        } catch (TypeError $ex) {
            // PHP engine can't handle exceptions from __toString()
            return '';
        }
    }
}
Core32/SecretStream/State.php000064400000007110152331075510011774 0ustar00<?php

/**
 * Class ParagonIE_Sodium_Core32_SecretStream_State
 */
class ParagonIE_Sodium_Core32_SecretStream_State
{
    /** @var string $key */
    protected $key;

    /** @var int $counter */
    protected $counter;

    /** @var string $nonce */
    protected $nonce;

    /** @var string $_pad */
    protected $_pad;

    /**
     * ParagonIE_Sodium_Core32_SecretStream_State constructor.
     * @param string $key
     * @param string|null $nonce
     */
    public function __construct($key, $nonce = null)
    {
        $this->key = $key;
        $this->counter = 1;
        if (is_null($nonce)) {
            $nonce = str_repeat("\0", 12);
        }
        $this->nonce = str_pad($nonce, 12, "\0", STR_PAD_RIGHT);;
        $this->_pad = str_repeat("\0", 4);
    }

    /**
     * @return self
     */
    public function counterReset()
    {
        $this->counter = 1;
        $this->_pad = str_repeat("\0", 4);
        return $this;
    }

    /**
     * @return string
     */
    public function getKey()
    {
        return $this->key;
    }

    /**
     * @return string
     */
    public function getCounter()
    {
        return ParagonIE_Sodium_Core32_Util::store32_le($this->counter);
    }

    /**
     * @return string
     */
    public function getNonce()
    {
        if (!is_string($this->nonce)) {
            $this->nonce = str_repeat("\0", 12);
        }
        if (ParagonIE_Sodium_Core32_Util::strlen($this->nonce) !== 12) {
            $this->nonce = str_pad($this->nonce, 12, "\0", STR_PAD_RIGHT);
        }
        return $this->nonce;
    }

    /**
     * @return string
     */
    public function getCombinedNonce()
    {
        return $this->getCounter() .
            ParagonIE_Sodium_Core32_Util::substr($this->getNonce(), 0, 8);
    }

    /**
     * @return self
     */
    public function incrementCounter()
    {
        ++$this->counter;
        return $this;
    }

    /**
     * @return bool
     */
    public function needsRekey()
    {
        return ($this->counter & 0xffff) === 0;
    }

    /**
     * @param string $newKeyAndNonce
     * @return self
     */
    public function rekey($newKeyAndNonce)
    {
        $this->key = ParagonIE_Sodium_Core32_Util::substr($newKeyAndNonce, 0, 32);
        $this->nonce = str_pad(
            ParagonIE_Sodium_Core32_Util::substr($newKeyAndNonce, 32),
            12,
            "\0",
            STR_PAD_RIGHT
        );
        return $this;
    }

    /**
     * @param string $str
     * @return self
     */
    public function xorNonce($str)
    {
        $this->nonce = ParagonIE_Sodium_Core32_Util::xorStrings(
            $this->getNonce(),
            str_pad(
                ParagonIE_Sodium_Core32_Util::substr($str, 0, 8),
                12,
                "\0",
                STR_PAD_RIGHT
            )
        );
        return $this;
    }

    /**
     * @param string $string
     * @return self
     */
    public static function fromString($string)
    {
        $state = new ParagonIE_Sodium_Core32_SecretStream_State(
            ParagonIE_Sodium_Core32_Util::substr($string, 0, 32)
        );
        $state->counter = ParagonIE_Sodium_Core32_Util::load_4(
            ParagonIE_Sodium_Core32_Util::substr($string, 32, 4)
        );
        $state->nonce = ParagonIE_Sodium_Core32_Util::substr($string, 36, 12);
        $state->_pad = ParagonIE_Sodium_Core32_Util::substr($string, 48, 8);
        return $state;
    }

    /**
     * @return string
     */
    public function toString()
    {
        return $this->key .
            $this->getCounter() .
            $this->nonce .
            $this->_pad;
    }
}
Core32/SipHash.php000064400000014725152331075510007664 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_SipHash', false)) {
    return;
}

/**
 * Class ParagonIE_SodiumCompat_Core32_SipHash
 *
 * Only uses 32-bit arithmetic, while the original SipHash used 64-bit integers
 */
class ParagonIE_Sodium_Core32_SipHash extends ParagonIE_Sodium_Core32_Util
{
    /**
     * @internal You should not use this directly from another application
     *
     * @param array<int, ParagonIE_Sodium_Core32_Int64> $v
     * @return array<int, ParagonIE_Sodium_Core32_Int64>
     */
    public static function sipRound(array $v)
    {
        # v0 += v1;
        $v[0] = $v[0]->addInt64($v[1]);

        # v1 = ROTL(v1, 13);
        $v[1] = $v[1]->rotateLeft(13);

        #  v1 ^= v0;
        $v[1] = $v[1]->xorInt64($v[0]);

        #  v0=ROTL(v0,32);
        $v[0] = $v[0]->rotateLeft(32);

        # v2 += v3;
        $v[2] = $v[2]->addInt64($v[3]);

        # v3=ROTL(v3,16);
        $v[3] = $v[3]->rotateLeft(16);

        #  v3 ^= v2;
        $v[3] = $v[3]->xorInt64($v[2]);

        # v0 += v3;
        $v[0] = $v[0]->addInt64($v[3]);

        # v3=ROTL(v3,21);
        $v[3] = $v[3]->rotateLeft(21);

        # v3 ^= v0;
        $v[3] = $v[3]->xorInt64($v[0]);

        # v2 += v1;
        $v[2] = $v[2]->addInt64($v[1]);

        # v1=ROTL(v1,17);
        $v[1] = $v[1]->rotateLeft(17);

        #  v1 ^= v2;
        $v[1] = $v[1]->xorInt64($v[2]);

        # v2=ROTL(v2,32)
        $v[2] = $v[2]->rotateLeft(32);

        return $v;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param string $in
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sipHash24($in, $key)
    {
        $inlen = self::strlen($in);

        # /* "somepseudorandomlygeneratedbytes" */
        # u64 v0 = 0x736f6d6570736575ULL;
        # u64 v1 = 0x646f72616e646f6dULL;
        # u64 v2 = 0x6c7967656e657261ULL;
        # u64 v3 = 0x7465646279746573ULL;
        $v = array(
            new ParagonIE_Sodium_Core32_Int64(
                array(0x736f, 0x6d65, 0x7073, 0x6575)
            ),
            new ParagonIE_Sodium_Core32_Int64(
                array(0x646f, 0x7261, 0x6e64, 0x6f6d)
            ),
            new ParagonIE_Sodium_Core32_Int64(
                array(0x6c79, 0x6765, 0x6e65, 0x7261)
            ),
            new ParagonIE_Sodium_Core32_Int64(
                array(0x7465, 0x6462, 0x7974, 0x6573)
            )
        );

        # u64 k0 = LOAD64_LE( k );
        # u64 k1 = LOAD64_LE( k + 8 );
        $k = array(
            ParagonIE_Sodium_Core32_Int64::fromReverseString(
                self::substr($key, 0, 8)
            ),
            ParagonIE_Sodium_Core32_Int64::fromReverseString(
                self::substr($key, 8, 8)
            )
        );

        # b = ( ( u64 )inlen ) << 56;
        $b = new ParagonIE_Sodium_Core32_Int64(
            array(($inlen << 8) & 0xffff, 0, 0, 0)
        );

        # v3 ^= k1;
        $v[3] = $v[3]->xorInt64($k[1]);
        # v2 ^= k0;
        $v[2] = $v[2]->xorInt64($k[0]);
        # v1 ^= k1;
        $v[1] = $v[1]->xorInt64($k[1]);
        # v0 ^= k0;
        $v[0] = $v[0]->xorInt64($k[0]);

        $left = $inlen;
        # for ( ; in != end; in += 8 )
        while ($left >= 8) {
            # m = LOAD64_LE( in );
            $m = ParagonIE_Sodium_Core32_Int64::fromReverseString(
                self::substr($in, 0, 8)
            );

            # v3 ^= m;
            $v[3] = $v[3]->xorInt64($m);

            # SIPROUND;
            # SIPROUND;
            $v = self::sipRound($v);
            $v = self::sipRound($v);

            # v0 ^= m;
            $v[0] = $v[0]->xorInt64($m);

            $in = self::substr($in, 8);
            $left -= 8;
        }

        # switch( left )
        #  {
        #     case 7: b |= ( ( u64 )in[ 6] )  << 48;
        #     case 6: b |= ( ( u64 )in[ 5] )  << 40;
        #     case 5: b |= ( ( u64 )in[ 4] )  << 32;
        #     case 4: b |= ( ( u64 )in[ 3] )  << 24;
        #     case 3: b |= ( ( u64 )in[ 2] )  << 16;
        #     case 2: b |= ( ( u64 )in[ 1] )  <<  8;
        #     case 1: b |= ( ( u64 )in[ 0] ); break;
        #     case 0: break;
        # }
        switch ($left) {
            case 7:
                $b = $b->orInt64(
                    ParagonIE_Sodium_Core32_Int64::fromInts(
                        0, self::chrToInt($in[6]) << 16
                    )
                );
            case 6:
                $b = $b->orInt64(
                    ParagonIE_Sodium_Core32_Int64::fromInts(
                        0, self::chrToInt($in[5]) << 8
                    )
                );
            case 5:
                $b = $b->orInt64(
                    ParagonIE_Sodium_Core32_Int64::fromInts(
                        0, self::chrToInt($in[4])
                    )
                );
            case 4:
                $b = $b->orInt64(
                    ParagonIE_Sodium_Core32_Int64::fromInts(
                        self::chrToInt($in[3]) << 24, 0
                    )
                );
            case 3:
                $b = $b->orInt64(
                    ParagonIE_Sodium_Core32_Int64::fromInts(
                        self::chrToInt($in[2]) << 16, 0
                    )
                );
            case 2:
                $b = $b->orInt64(
                    ParagonIE_Sodium_Core32_Int64::fromInts(
                        self::chrToInt($in[1]) << 8, 0
                    )
                );
            case 1:
                $b = $b->orInt64(
                    ParagonIE_Sodium_Core32_Int64::fromInts(
                        self::chrToInt($in[0]), 0
                    )
                );
            case 0:
                break;
        }

        # v3 ^= b;
        $v[3] = $v[3]->xorInt64($b);

        # SIPROUND;
        # SIPROUND;
        $v = self::sipRound($v);
        $v = self::sipRound($v);

        # v0 ^= b;
        $v[0] = $v[0]->xorInt64($b);

        // Flip the lower 8 bits of v2 which is ($v[4], $v[5]) in our implementation
        # v2 ^= 0xff;
        $v[2]->limbs[3] ^= 0xff;

        # SIPROUND;
        # SIPROUND;
        # SIPROUND;
        # SIPROUND;
        $v = self::sipRound($v);
        $v = self::sipRound($v);
        $v = self::sipRound($v);
        $v = self::sipRound($v);

        # b = v0 ^ v1 ^ v2 ^ v3;
        # STORE64_LE( out, b );
        return $v[0]
            ->xorInt64($v[1])
            ->xorInt64($v[2])
            ->xorInt64($v[3])
            ->toReverseString();
    }
}
Core32/Int64.php000064400000074664152331075510007241 0ustar00<?php

/**
 * Class ParagonIE_Sodium_Core32_Int64
 *
 * Encapsulates a 64-bit integer.
 *
 * These are immutable. It always returns a new instance.
 */
class ParagonIE_Sodium_Core32_Int64
{
    /**
     * @var array<int, int> - four 16-bit integers
     */
    public $limbs = array(0, 0, 0, 0);

    /**
     * @var int
     */
    public $overflow = 0;

    /**
     * @var bool
     */
    public $unsignedInt = false;

    /**
     * ParagonIE_Sodium_Core32_Int64 constructor.
     * @param array $array
     * @param bool $unsignedInt
     */
    public function __construct($array = array(0, 0, 0, 0), $unsignedInt = false)
    {
        $this->limbs = array(
            (int) $array[0],
            (int) $array[1],
            (int) $array[2],
            (int) $array[3]
        );
        $this->overflow = 0;
        $this->unsignedInt = $unsignedInt;
    }

    /**
     * Adds two int64 objects
     *
     * @param ParagonIE_Sodium_Core32_Int64 $addend
     * @return ParagonIE_Sodium_Core32_Int64
     */
    public function addInt64(ParagonIE_Sodium_Core32_Int64 $addend)
    {
        $i0 = $this->limbs[0];
        $i1 = $this->limbs[1];
        $i2 = $this->limbs[2];
        $i3 = $this->limbs[3];
        $j0 = $addend->limbs[0];
        $j1 = $addend->limbs[1];
        $j2 = $addend->limbs[2];
        $j3 = $addend->limbs[3];

        $r3 = $i3 + ($j3 & 0xffff);
        $carry = $r3 >> 16;

        $r2 = $i2 + ($j2 & 0xffff) + $carry;
        $carry = $r2 >> 16;

        $r1 = $i1 + ($j1 & 0xffff) + $carry;
        $carry = $r1 >> 16;

        $r0 = $i0 + ($j0 & 0xffff) + $carry;
        $carry = $r0 >> 16;

        $r0 &= 0xffff;
        $r1 &= 0xffff;
        $r2 &= 0xffff;
        $r3 &= 0xffff;

        $return = new ParagonIE_Sodium_Core32_Int64(
            array($r0, $r1, $r2, $r3)
        );
        $return->overflow = $carry;
        $return->unsignedInt = $this->unsignedInt;
        return $return;
    }

    /**
     * Adds a normal integer to an int64 object
     *
     * @param int $int
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws SodiumException
     * @throws TypeError
     */
    public function addInt($int)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($int, 'int', 1);
        /** @var int $int */
        $int = (int) $int;

        $i0 = $this->limbs[0];
        $i1 = $this->limbs[1];
        $i2 = $this->limbs[2];
        $i3 = $this->limbs[3];

        $r3 = $i3 + ($int & 0xffff);
        $carry = $r3 >> 16;

        $r2 = $i2 + (($int >> 16) & 0xffff) + $carry;
        $carry = $r2 >> 16;

        $r1 = $i1 + $carry;
        $carry = $r1 >> 16;

        $r0 = $i0 + $carry;
        $carry = $r0 >> 16;

        $r0 &= 0xffff;
        $r1 &= 0xffff;
        $r2 &= 0xffff;
        $r3 &= 0xffff;
        $return = new ParagonIE_Sodium_Core32_Int64(
            array($r0, $r1, $r2, $r3)
        );
        $return->overflow = $carry;
        $return->unsignedInt = $this->unsignedInt;
        return $return;
    }

    /**
     * @param int $b
     * @return int
     */
    public function compareInt($b = 0)
    {
        $gt = 0;
        $eq = 1;

        $i = 4;
        $j = 0;
        while ($i > 0) {
            --$i;
            /** @var int $x1 */
            $x1 = $this->limbs[$i];
            /** @var int $x2 */
            $x2 = ($b >> ($j << 4)) & 0xffff;
            /** int */
            $gt |= (($x2 - $x1) >> 8) & $eq;
            /** int */
            $eq &= (($x2 ^ $x1) - 1) >> 8;
        }
        return ($gt + $gt - $eq) + 1;
    }

    /**
     * @param int $b
     * @return bool
     */
    public function isGreaterThan($b = 0)
    {
        return $this->compareInt($b) > 0;
    }

    /**
     * @param int $b
     * @return bool
     */
    public function isLessThanInt($b = 0)
    {
        return $this->compareInt($b) < 0;
    }

    /**
     * @param int $hi
     * @param int $lo
     * @return ParagonIE_Sodium_Core32_Int64
     */
    public function mask64($hi = 0, $lo = 0)
    {
        /** @var int $a */
        $a = ($hi >> 16) & 0xffff;
        /** @var int $b */
        $b = ($hi) & 0xffff;
        /** @var int $c */
        $c = ($lo >> 16) & 0xffff;
        /** @var int $d */
        $d = ($lo & 0xffff);
        return new ParagonIE_Sodium_Core32_Int64(
            array(
                $this->limbs[0] & $a,
                $this->limbs[1] & $b,
                $this->limbs[2] & $c,
                $this->limbs[3] & $d
            ),
            $this->unsignedInt
        );
    }

    /**
     * @param int $int
     * @param int $size
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedAssignment
     */
    public function mulInt($int = 0, $size = 0)
    {
        if (ParagonIE_Sodium_Compat::$fastMult) {
            return $this->mulIntFast($int);
        }
        ParagonIE_Sodium_Core32_Util::declareScalarType($int, 'int', 1);
        ParagonIE_Sodium_Core32_Util::declareScalarType($size, 'int', 2);
        /** @var int $int */
        $int = (int) $int;
        /** @var int $size */
        $size = (int) $size;

        if (!$size) {
            $size = 63;
        }

        $a = clone $this;
        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->unsignedInt = $this->unsignedInt;

        // Initialize:
        $ret0 = 0;
        $ret1 = 0;
        $ret2 = 0;
        $ret3 = 0;
        $a0 = $a->limbs[0];
        $a1 = $a->limbs[1];
        $a2 = $a->limbs[2];
        $a3 = $a->limbs[3];

        /** @var int $size */
        /** @var int $i */
        for ($i = $size; $i >= 0; --$i) {
            $mask = -($int & 1);
            $x0 = $a0 & $mask;
            $x1 = $a1 & $mask;
            $x2 = $a2 & $mask;
            $x3 = $a3 & $mask;

            $ret3 += $x3;
            $c = $ret3 >> 16;

            $ret2 += $x2 + $c;
            $c = $ret2 >> 16;

            $ret1 += $x1 + $c;
            $c = $ret1 >> 16;

            $ret0 += $x0 + $c;

            $ret0 &= 0xffff;
            $ret1 &= 0xffff;
            $ret2 &= 0xffff;
            $ret3 &= 0xffff;

            $a3 = $a3 << 1;
            $x3 = $a3 >> 16;
            $a2 = ($a2 << 1) | $x3;
            $x2 = $a2 >> 16;
            $a1 = ($a1 << 1) | $x2;
            $x1 = $a1 >> 16;
            $a0 = ($a0 << 1) | $x1;
            $a0 &= 0xffff;
            $a1 &= 0xffff;
            $a2 &= 0xffff;
            $a3 &= 0xffff;

            $int >>= 1;
        }
        $return->limbs[0] = $ret0;
        $return->limbs[1] = $ret1;
        $return->limbs[2] = $ret2;
        $return->limbs[3] = $ret3;
        return $return;
    }

    /**
     * @param ParagonIE_Sodium_Core32_Int64 $A
     * @param ParagonIE_Sodium_Core32_Int64 $B
     * @return array<int, ParagonIE_Sodium_Core32_Int64>
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedInferredReturnType
     */
    public static function ctSelect(
        ParagonIE_Sodium_Core32_Int64 $A,
        ParagonIE_Sodium_Core32_Int64 $B
    ) {
        $a = clone $A;
        $b = clone $B;
        /** @var int $aNeg */
        $aNeg = ($a->limbs[0] >> 15) & 1;
        /** @var int $bNeg */
        $bNeg = ($b->limbs[0] >> 15) & 1;
        /** @var int $m */
        $m = (-($aNeg & $bNeg)) | 1;
        /** @var int $swap */
        $swap = $bNeg & ~$aNeg;
        /** @var int $d */
        $d = -$swap;

        /*
        if ($bNeg && !$aNeg) {
            $a = clone $int;
            $b = clone $this;
        } elseif($bNeg && $aNeg) {
            $a = $this->mulInt(-1);
            $b = $int->mulInt(-1);
        }
         */
        $x = $a->xorInt64($b)->mask64($d, $d);
        return array(
            $a->xorInt64($x)->mulInt($m),
            $b->xorInt64($x)->mulInt($m)
        );
    }

    /**
     * @param array<int, int> $a
     * @param array<int, int> $b
     * @param int $baseLog2
     * @return array<int, int>
     */
    public function multiplyLong(array $a, array $b, $baseLog2 = 16)
    {
        $a_l = count($a);
        $b_l = count($b);
        /** @var array<int, int> $r */
        $r = array_fill(0, $a_l + $b_l + 1, 0);
        $base = 1 << $baseLog2;
        for ($i = 0; $i < $a_l; ++$i) {
            $a_i = $a[$i];
            for ($j = 0; $j < $a_l; ++$j) {
                $b_j = $b[$j];
                $product = ($a_i * $b_j) + $r[$i + $j];
                $carry = ($product >> $baseLog2 & 0xffff);
                $r[$i + $j] = ($product - (int) ($carry * $base)) & 0xffff;
                $r[$i + $j + 1] += $carry;
            }
        }
        return array_slice($r, 0, 5);
    }

    /**
     * @param int $int
     * @return ParagonIE_Sodium_Core32_Int64
     */
    public function mulIntFast($int)
    {
        // Handle negative numbers
        $aNeg = ($this->limbs[0] >> 15) & 1;
        $bNeg = ($int >> 31) & 1;
        $a = array_reverse($this->limbs);
        $b = array(
            $int & 0xffff,
            ($int >> 16) & 0xffff,
            -$bNeg & 0xffff,
            -$bNeg & 0xffff
        );
        if ($aNeg) {
            for ($i = 0; $i < 4; ++$i) {
                $a[$i] = ($a[$i] ^ 0xffff) & 0xffff;
            }
            ++$a[0];
        }
        if ($bNeg) {
            for ($i = 0; $i < 4; ++$i) {
                $b[$i] = ($b[$i] ^ 0xffff) & 0xffff;
            }
            ++$b[0];
        }
        // Multiply
        $res = $this->multiplyLong($a, $b);

        // Re-apply negation to results
        if ($aNeg !== $bNeg) {
            for ($i = 0; $i < 4; ++$i) {
                $res[$i] = (0xffff ^ $res[$i]) & 0xffff;
            }
            // Handle integer overflow
            $c = 1;
            for ($i = 0; $i < 4; ++$i) {
                $res[$i] += $c;
                $c = $res[$i] >> 16;
                $res[$i] &= 0xffff;
            }
        }

        // Return our values
        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->limbs = array(
            $res[3] & 0xffff,
            $res[2] & 0xffff,
            $res[1] & 0xffff,
            $res[0] & 0xffff
        );
        if (count($res) > 4) {
            $return->overflow = $res[4] & 0xffff;
        }
        $return->unsignedInt = $this->unsignedInt;
        return $return;
    }

    /**
     * @param ParagonIE_Sodium_Core32_Int64 $right
     * @return ParagonIE_Sodium_Core32_Int64
     */
    public function mulInt64Fast(ParagonIE_Sodium_Core32_Int64 $right)
    {
        $aNeg = ($this->limbs[0] >> 15) & 1;
        $bNeg = ($right->limbs[0] >> 15) & 1;

        $a = array_reverse($this->limbs);
        $b = array_reverse($right->limbs);
        if ($aNeg) {
            for ($i = 0; $i < 4; ++$i) {
                $a[$i] = ($a[$i] ^ 0xffff) & 0xffff;
            }
            ++$a[0];
        }
        if ($bNeg) {
            for ($i = 0; $i < 4; ++$i) {
                $b[$i] = ($b[$i] ^ 0xffff) & 0xffff;
            }
            ++$b[0];
        }
        $res = $this->multiplyLong($a, $b);
        if ($aNeg !== $bNeg) {
            if ($aNeg !== $bNeg) {
                for ($i = 0; $i < 4; ++$i) {
                    $res[$i] = ($res[$i] ^ 0xffff) & 0xffff;
                }
                $c = 1;
                for ($i = 0; $i < 4; ++$i) {
                    $res[$i] += $c;
                    $c = $res[$i] >> 16;
                    $res[$i] &= 0xffff;
                }
            }
        }
        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->limbs = array(
            $res[3] & 0xffff,
            $res[2] & 0xffff,
            $res[1] & 0xffff,
            $res[0] & 0xffff
        );
        if (count($res) > 4) {
            $return->overflow = $res[4];
        }
        return $return;
    }

    /**
     * @param ParagonIE_Sodium_Core32_Int64 $int
     * @param int $size
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedAssignment
     */
    public function mulInt64(ParagonIE_Sodium_Core32_Int64 $int, $size = 0)
    {
        if (ParagonIE_Sodium_Compat::$fastMult) {
            return $this->mulInt64Fast($int);
        }
        ParagonIE_Sodium_Core32_Util::declareScalarType($size, 'int', 2);
        if (!$size) {
            $size = 63;
        }
        list($a, $b) = self::ctSelect($this, $int);

        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->unsignedInt = $this->unsignedInt;

        // Initialize:
        $ret0 = 0;
        $ret1 = 0;
        $ret2 = 0;
        $ret3 = 0;
        $a0 = $a->limbs[0];
        $a1 = $a->limbs[1];
        $a2 = $a->limbs[2];
        $a3 = $a->limbs[3];
        $b0 = $b->limbs[0];
        $b1 = $b->limbs[1];
        $b2 = $b->limbs[2];
        $b3 = $b->limbs[3];

        /** @var int $size */
        /** @var int $i */
        for ($i = (int) $size; $i >= 0; --$i) {
            $mask = -($b3 & 1);
            $x0 = $a0 & $mask;
            $x1 = $a1 & $mask;
            $x2 = $a2 & $mask;
            $x3 = $a3 & $mask;

            $ret3 += $x3;
            $c = $ret3 >> 16;

            $ret2 += $x2 + $c;
            $c = $ret2 >> 16;

            $ret1 += $x1 + $c;
            $c = $ret1 >> 16;

            $ret0 += $x0 + $c;

            $ret0 &= 0xffff;
            $ret1 &= 0xffff;
            $ret2 &= 0xffff;
            $ret3 &= 0xffff;

            $a3 = $a3 << 1;
            $x3 = $a3 >> 16;
            $a2 = ($a2 << 1) | $x3;
            $x2 = $a2 >> 16;
            $a1 = ($a1 << 1) | $x2;
            $x1 = $a1 >> 16;
            $a0 = ($a0 << 1) | $x1;
            $a0 &= 0xffff;
            $a1 &= 0xffff;
            $a2 &= 0xffff;
            $a3 &= 0xffff;

            $x0 = ($b0 & 1) << 16;
            $x1 = ($b1 & 1) << 16;
            $x2 = ($b2 & 1) << 16;

            $b0 = ($b0 >> 1);
            $b1 = (($b1 | $x0) >> 1);
            $b2 = (($b2 | $x1) >> 1);
            $b3 = (($b3 | $x2) >> 1);

            $b0 &= 0xffff;
            $b1 &= 0xffff;
            $b2 &= 0xffff;
            $b3 &= 0xffff;

        }
        $return->limbs[0] = $ret0;
        $return->limbs[1] = $ret1;
        $return->limbs[2] = $ret2;
        $return->limbs[3] = $ret3;

        return $return;
    }

    /**
     * OR this 64-bit integer with another.
     *
     * @param ParagonIE_Sodium_Core32_Int64 $b
     * @return ParagonIE_Sodium_Core32_Int64
     */
    public function orInt64(ParagonIE_Sodium_Core32_Int64 $b)
    {
        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->unsignedInt = $this->unsignedInt;
        $return->limbs = array(
            (int) ($this->limbs[0] | $b->limbs[0]),
            (int) ($this->limbs[1] | $b->limbs[1]),
            (int) ($this->limbs[2] | $b->limbs[2]),
            (int) ($this->limbs[3] | $b->limbs[3])
        );
        return $return;
    }

    /**
     * @param int $c
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArrayAccess
     */
    public function rotateLeft($c = 0)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($c, 'int', 1);
        /** @var int $c */
        $c = (int) $c;

        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->unsignedInt = $this->unsignedInt;
        $c &= 63;
        if ($c === 0) {
            // NOP, but we want a copy.
            $return->limbs = $this->limbs;
        } else {
            /** @var array<int, int> $limbs */
            $limbs =& $return->limbs;

            /** @var array<int, int> $myLimbs */
            $myLimbs =& $this->limbs;

            /** @var int $idx_shift */
            $idx_shift = ($c >> 4) & 3;
            /** @var int $sub_shift */
            $sub_shift = $c & 15;

            for ($i = 3; $i >= 0; --$i) {
                /** @var int $j */
                $j = ($i + $idx_shift) & 3;
                /** @var int $k */
                $k = ($i + $idx_shift + 1) & 3;
                $limbs[$i] = (int) (
                    (
                        ((int) ($myLimbs[$j]) << $sub_shift)
                            |
                        ((int) ($myLimbs[$k]) >> (16 - $sub_shift))
                    ) & 0xffff
                );
            }
        }
        return $return;
    }

    /**
     * Rotate to the right
     *
     * @param int $c
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws SodiumException
     * @throws TypeError
     * @psalm-suppress MixedArrayAccess
     */
    public function rotateRight($c = 0)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($c, 'int', 1);
        /** @var int $c */
        $c = (int) $c;

        /** @var ParagonIE_Sodium_Core32_Int64 $return */
        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->unsignedInt = $this->unsignedInt;
        $c &= 63;
        /** @var int $c */
        if ($c === 0) {
            // NOP, but we want a copy.
            $return->limbs = $this->limbs;
        } else {
            /** @var array<int, int> $limbs */
            $limbs =& $return->limbs;

            /** @var array<int, int> $myLimbs */
            $myLimbs =& $this->limbs;

            /** @var int $idx_shift */
            $idx_shift = ($c >> 4) & 3;
            /** @var int $sub_shift */
            $sub_shift = $c & 15;

            for ($i = 3; $i >= 0; --$i) {
                /** @var int $j */
                $j = ($i - $idx_shift) & 3;
                /** @var int $k */
                $k = ($i - $idx_shift - 1) & 3;
                $limbs[$i] = (int) (
                    (
                        ((int) ($myLimbs[$j]) >> (int) ($sub_shift))
                            |
                        ((int) ($myLimbs[$k]) << (16 - (int) ($sub_shift)))
                    ) & 0xffff
                );
            }
        }
        return $return;
    }
    /**
     * @param int $c
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws SodiumException
     * @throws TypeError
     */
    public function shiftLeft($c = 0)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($c, 'int', 1);
        /** @var int $c */
        $c = (int) $c;

        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->unsignedInt = $this->unsignedInt;
        $c &= 63;

        if ($c >= 16) {
            if ($c >= 48) {
                $return->limbs = array(
                    $this->limbs[3], 0, 0, 0
                );
            } elseif ($c >= 32) {
                $return->limbs = array(
                    $this->limbs[2], $this->limbs[3], 0, 0
                );
            } else {
                $return->limbs = array(
                    $this->limbs[1], $this->limbs[2], $this->limbs[3], 0
                );
            }
            return $return->shiftLeft($c & 15);
        }
        if ($c === 0) {
            $return->limbs = $this->limbs;
        } elseif ($c < 0) {
            /** @var int $c */
            return $this->shiftRight(-$c);
        } else {
            if (!is_int($c)) {
                throw new TypeError();
            }
            /** @var int $carry */
            $carry = 0;
            for ($i = 3; $i >= 0; --$i) {
                /** @var int $tmp */
                $tmp = ($this->limbs[$i] << $c) | ($carry & 0xffff);
                $return->limbs[$i] = (int) ($tmp & 0xffff);
                /** @var int $carry */
                $carry = $tmp >> 16;
            }
        }
        return $return;
    }

    /**
     * @param int $c
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws SodiumException
     * @throws TypeError
     */
    public function shiftRight($c = 0)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($c, 'int', 1);
        $c = (int) $c;
        /** @var int $c */
        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->unsignedInt = $this->unsignedInt;
        $c &= 63;

        $negative = -(($this->limbs[0] >> 15) & 1);
        if ($c >= 16) {
            if ($c >= 48) {
                $return->limbs = array(
                    (int) ($negative & 0xffff),
                    (int) ($negative & 0xffff),
                    (int) ($negative & 0xffff),
                    (int) $this->limbs[0]
                );
            } elseif ($c >= 32) {
                $return->limbs = array(
                    (int) ($negative & 0xffff),
                    (int) ($negative & 0xffff),
                    (int) $this->limbs[0],
                    (int) $this->limbs[1]
                );
            } else {
                $return->limbs = array(
                    (int) ($negative & 0xffff),
                    (int) $this->limbs[0],
                    (int) $this->limbs[1],
                    (int) $this->limbs[2]
                );
            }
            return $return->shiftRight($c & 15);
        }

        if ($c === 0) {
            $return->limbs = $this->limbs;
        } elseif ($c < 0) {
            return $this->shiftLeft(-$c);
        } else {
            if (!is_int($c)) {
                throw new TypeError();
            }
            /** @var int $carryRight */
            $carryRight = ($negative & 0xffff);
            $mask = (int) (((1 << ($c + 1)) - 1) & 0xffff);
            for ($i = 0; $i < 4; ++$i) {
                $return->limbs[$i] = (int) (
                    (($this->limbs[$i] >> $c) | ($carryRight << (16 - $c))) & 0xffff
                );
                $carryRight = (int) ($this->limbs[$i] & $mask);
            }
        }
        return $return;
    }


    /**
     * Subtract a normal integer from an int64 object.
     *
     * @param int $int
     * @return ParagonIE_Sodium_Core32_Int64
     * @throws SodiumException
     * @throws TypeError
     */
    public function subInt($int)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($int, 'int', 1);
        $int = (int) $int;

        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->unsignedInt = $this->unsignedInt;

        /** @var int $carry */
        $carry = 0;
        for ($i = 3; $i >= 0; --$i) {
            /** @var int $tmp */
            $tmp = $this->limbs[$i] - (($int >> 16) & 0xffff) + $carry;
            /** @var int $carry */
            $carry = $tmp >> 16;
            $return->limbs[$i] = (int) ($tmp & 0xffff);
        }
        return $return;
    }

    /**
     * The difference between two Int64 objects.
     *
     * @param ParagonIE_Sodium_Core32_Int64 $b
     * @return ParagonIE_Sodium_Core32_Int64
     */
    public function subInt64(ParagonIE_Sodium_Core32_Int64 $b)
    {
        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->unsignedInt = $this->unsignedInt;
        /** @var int $carry */
        $carry = 0;
        for ($i = 3; $i >= 0; --$i) {
            /** @var int $tmp */
            $tmp = $this->limbs[$i] - $b->limbs[$i] + $carry;
            /** @var int $carry */
            $carry = ($tmp >> 16);
            $return->limbs[$i] = (int) ($tmp & 0xffff);
        }
        return $return;
    }

    /**
     * XOR this 64-bit integer with another.
     *
     * @param ParagonIE_Sodium_Core32_Int64 $b
     * @return ParagonIE_Sodium_Core32_Int64
     */
    public function xorInt64(ParagonIE_Sodium_Core32_Int64 $b)
    {
        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->unsignedInt = $this->unsignedInt;
        $return->limbs = array(
            (int) ($this->limbs[0] ^ $b->limbs[0]),
            (int) ($this->limbs[1] ^ $b->limbs[1]),
            (int) ($this->limbs[2] ^ $b->limbs[2]),
            (int) ($this->limbs[3] ^ $b->limbs[3])
        );
        return $return;
    }

    /**
     * @param int $low
     * @param int $high
     * @return self
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fromInts($low, $high)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($low, 'int', 1);
        ParagonIE_Sodium_Core32_Util::declareScalarType($high, 'int', 2);

        $high = (int) $high;
        $low = (int) $low;
        return new ParagonIE_Sodium_Core32_Int64(
            array(
                (int) (($high >> 16) & 0xffff),
                (int) ($high & 0xffff),
                (int) (($low >> 16) & 0xffff),
                (int) ($low & 0xffff)
            )
        );
    }

    /**
     * @param int $low
     * @return self
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fromInt($low)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($low, 'int', 1);
        $low = (int) $low;

        return new ParagonIE_Sodium_Core32_Int64(
            array(
                0,
                0,
                (int) (($low >> 16) & 0xffff),
                (int) ($low & 0xffff)
            )
        );
    }

    /**
     * @return int
     */
    public function toInt()
    {
        return (int) (
            (($this->limbs[2] & 0xffff) << 16)
                |
            ($this->limbs[3] & 0xffff)
        );
    }

    /**
     * @param string $string
     * @return self
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fromString($string)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($string, 'string', 1);
        $string = (string) $string;
        if (ParagonIE_Sodium_Core32_Util::strlen($string) !== 8) {
            throw new RangeException(
                'String must be 8 bytes; ' . ParagonIE_Sodium_Core32_Util::strlen($string) . ' given.'
            );
        }
        $return = new ParagonIE_Sodium_Core32_Int64();

        $return->limbs[0]  = (int) ((ParagonIE_Sodium_Core32_Util::chrToInt($string[0]) & 0xff) << 8);
        $return->limbs[0] |= (ParagonIE_Sodium_Core32_Util::chrToInt($string[1]) & 0xff);
        $return->limbs[1]  = (int) ((ParagonIE_Sodium_Core32_Util::chrToInt($string[2]) & 0xff) << 8);
        $return->limbs[1] |= (ParagonIE_Sodium_Core32_Util::chrToInt($string[3]) & 0xff);
        $return->limbs[2]  = (int) ((ParagonIE_Sodium_Core32_Util::chrToInt($string[4]) & 0xff) << 8);
        $return->limbs[2] |= (ParagonIE_Sodium_Core32_Util::chrToInt($string[5]) & 0xff);
        $return->limbs[3]  = (int) ((ParagonIE_Sodium_Core32_Util::chrToInt($string[6]) & 0xff) << 8);
        $return->limbs[3] |= (ParagonIE_Sodium_Core32_Util::chrToInt($string[7]) & 0xff);
        return $return;
    }

    /**
     * @param string $string
     * @return self
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fromReverseString($string)
    {
        ParagonIE_Sodium_Core32_Util::declareScalarType($string, 'string', 1);
        $string = (string) $string;
        if (ParagonIE_Sodium_Core32_Util::strlen($string) !== 8) {
            throw new RangeException(
                'String must be 8 bytes; ' . ParagonIE_Sodium_Core32_Util::strlen($string) . ' given.'
            );
        }
        $return = new ParagonIE_Sodium_Core32_Int64();

        $return->limbs[0]  = (int) ((ParagonIE_Sodium_Core32_Util::chrToInt($string[7]) & 0xff) << 8);
        $return->limbs[0] |= (ParagonIE_Sodium_Core32_Util::chrToInt($string[6]) & 0xff);
        $return->limbs[1]  = (int) ((ParagonIE_Sodium_Core32_Util::chrToInt($string[5]) & 0xff) << 8);
        $return->limbs[1] |= (ParagonIE_Sodium_Core32_Util::chrToInt($string[4]) & 0xff);
        $return->limbs[2]  = (int) ((ParagonIE_Sodium_Core32_Util::chrToInt($string[3]) & 0xff) << 8);
        $return->limbs[2] |= (ParagonIE_Sodium_Core32_Util::chrToInt($string[2]) & 0xff);
        $return->limbs[3]  = (int) ((ParagonIE_Sodium_Core32_Util::chrToInt($string[1]) & 0xff) << 8);
        $return->limbs[3] |= (ParagonIE_Sodium_Core32_Util::chrToInt($string[0]) & 0xff);
        return $return;
    }

    /**
     * @return array<int, int>
     */
    public function toArray()
    {
        return array(
            (int) ((($this->limbs[0] & 0xffff) << 16) | ($this->limbs[1] & 0xffff)),
            (int) ((($this->limbs[2] & 0xffff) << 16) | ($this->limbs[3] & 0xffff))
        );
    }

    /**
     * @return ParagonIE_Sodium_Core32_Int32
     */
    public function toInt32()
    {
        $return = new ParagonIE_Sodium_Core32_Int32();
        $return->limbs[0] = (int) ($this->limbs[2]);
        $return->limbs[1] = (int) ($this->limbs[3]);
        $return->unsignedInt = $this->unsignedInt;
        $return->overflow = (int) (ParagonIE_Sodium_Core32_Util::abs($this->limbs[1], 16) & 0xffff);
        return $return;
    }

    /**
     * @return ParagonIE_Sodium_Core32_Int64
     */
    public function toInt64()
    {
        $return = new ParagonIE_Sodium_Core32_Int64();
        $return->limbs[0] = (int) ($this->limbs[0]);
        $return->limbs[1] = (int) ($this->limbs[1]);
        $return->limbs[2] = (int) ($this->limbs[2]);
        $return->limbs[3] = (int) ($this->limbs[3]);
        $return->unsignedInt = $this->unsignedInt;
        $return->overflow = ParagonIE_Sodium_Core32_Util::abs($this->overflow);
        return $return;
    }

    /**
     * @param bool $bool
     * @return self
     */
    public function setUnsignedInt($bool = false)
    {
        $this->unsignedInt = !empty($bool);
        return $this;
    }

    /**
     * @return string
     * @throws TypeError
     */
    public function toString()
    {
        return ParagonIE_Sodium_Core32_Util::intToChr(($this->limbs[0] >> 8) & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr($this->limbs[0] & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr(($this->limbs[1] >> 8) & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr($this->limbs[1] & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr(($this->limbs[2] >> 8) & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr($this->limbs[2] & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr(($this->limbs[3] >> 8) & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr($this->limbs[3] & 0xff);
    }

    /**
     * @return string
     * @throws TypeError
     */
    public function toReverseString()
    {
        return ParagonIE_Sodium_Core32_Util::intToChr($this->limbs[3] & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr(($this->limbs[3] >> 8) & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr($this->limbs[2] & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr(($this->limbs[2] >> 8) & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr($this->limbs[1] & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr(($this->limbs[1] >> 8) & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr($this->limbs[0] & 0xff) .
            ParagonIE_Sodium_Core32_Util::intToChr(($this->limbs[0] >> 8) & 0xff);
    }

    /**
     * @return string
     */
    public function __toString()
    {
        try {
            return $this->toString();
        } catch (TypeError $ex) {
            // PHP engine can't handle exceptions from __toString()
            return '';
        }
    }
}
Core32/Curve25519/H.php000064400000324375152331075510010213 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_Curve25519_H', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_Curve25519_H
 *
 * This just contains the constants in the ref10/base.h file
 */
class ParagonIE_Sodium_Core32_Curve25519_H extends ParagonIE_Sodium_Core32_Util
{
    /**
     * See: libsodium's crypto_core/curve25519/ref10/base.h
     *
     * @var array<int, array<int, array<int, array<int, int>>>> Basically, int[32][8][3][10]
     */
    protected static $base = array(
        array(
            array(
                array(25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605),
                array(-12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378),
                array(-8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546),
            ),
            array(
                array(-12815894, -12976347, -21581243, 11784320, -25355658, -2750717, -11717903, -3814571, -358445, -10211303),
                array(-21703237, 6903825, 27185491, 6451973, -29577724, -9554005, -15616551, 11189268, -26829678, -5319081),
                array(26966642, 11152617, 32442495, 15396054, 14353839, -12752335, -3128826, -9541118, -15472047, -4166697),
            ),
            array(
                array(15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024),
                array(16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574),
                array(30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357),
            ),
            array(
                array(-17036878, 13921892, 10945806, -6033431, 27105052, -16084379, -28926210, 15006023, 3284568, -6276540),
                array(23599295, -8306047, -11193664, -7687416, 13236774, 10506355, 7464579, 9656445, 13059162, 10374397),
                array(7798556, 16710257, 3033922, 2874086, 28997861, 2835604, 32406664, -3839045, -641708, -101325),
            ),
            array(
                array(10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380),
                array(4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306),
                array(19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942),
            ),
            array(
                array(-15371964, -12862754, 32573250, 4720197, -26436522, 5875511, -19188627, -15224819, -9818940, -12085777),
                array(-8549212, 109983, 15149363, 2178705, 22900618, 4543417, 3044240, -15689887, 1762328, 14866737),
                array(-18199695, -15951423, -10473290, 1707278, -17185920, 3916101, -28236412, 3959421, 27914454, 4383652),
            ),
            array(
                array(5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766),
                array(-30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701),
                array(28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300),
            ),
            array(
                array(14499471, -2729599, -33191113, -4254652, 28494862, 14271267, 30290735, 10876454, -33154098, 2381726),
                array(-7195431, -2655363, -14730155, 462251, -27724326, 3941372, -6236617, 3696005, -32300832, 15351955),
                array(27431194, 8222322, 16448760, -3907995, -18707002, 11938355, -32961401, -2970515, 29551813, 10109425),
            ),
        ),
        array(
            array(
                array(-13657040, -13155431, -31283750, 11777098, 21447386, 6519384, -2378284, -1627556, 10092783, -4764171),
                array(27939166, 14210322, 4677035, 16277044, -22964462, -12398139, -32508754, 12005538, -17810127, 12803510),
                array(17228999, -15661624, -1233527, 300140, -1224870, -11714777, 30364213, -9038194, 18016357, 4397660),
            ),
            array(
                array(-10958843, -7690207, 4776341, -14954238, 27850028, -15602212, -26619106, 14544525, -17477504, 982639),
                array(29253598, 15796703, -2863982, -9908884, 10057023, 3163536, 7332899, -4120128, -21047696, 9934963),
                array(5793303, 16271923, -24131614, -10116404, 29188560, 1206517, -14747930, 4559895, -30123922, -10897950),
            ),
            array(
                array(-27643952, -11493006, 16282657, -11036493, 28414021, -15012264, 24191034, 4541697, -13338309, 5500568),
                array(12650548, -1497113, 9052871, 11355358, -17680037, -8400164, -17430592, 12264343, 10874051, 13524335),
                array(25556948, -3045990, 714651, 2510400, 23394682, -10415330, 33119038, 5080568, -22528059, 5376628),
            ),
            array(
                array(-26088264, -4011052, -17013699, -3537628, -6726793, 1920897, -22321305, -9447443, 4535768, 1569007),
                array(-2255422, 14606630, -21692440, -8039818, 28430649, 8775819, -30494562, 3044290, 31848280, 12543772),
                array(-22028579, 2943893, -31857513, 6777306, 13784462, -4292203, -27377195, -2062731, 7718482, 14474653),
            ),
            array(
                array(2385315, 2454213, -22631320, 46603, -4437935, -15680415, 656965, -7236665, 24316168, -5253567),
                array(13741529, 10911568, -33233417, -8603737, -20177830, -1033297, 33040651, -13424532, -20729456, 8321686),
                array(21060490, -2212744, 15712757, -4336099, 1639040, 10656336, 23845965, -11874838, -9984458, 608372),
            ),
            array(
                array(-13672732, -15087586, -10889693, -7557059, -6036909, 11305547, 1123968, -6780577, 27229399, 23887),
                array(-23244140, -294205, -11744728, 14712571, -29465699, -2029617, 12797024, -6440308, -1633405, 16678954),
                array(-29500620, 4770662, -16054387, 14001338, 7830047, 9564805, -1508144, -4795045, -17169265, 4904953),
            ),
            array(
                array(24059557, 14617003, 19037157, -15039908, 19766093, -14906429, 5169211, 16191880, 2128236, -4326833),
                array(-16981152, 4124966, -8540610, -10653797, 30336522, -14105247, -29806336, 916033, -6882542, -2986532),
                array(-22630907, 12419372, -7134229, -7473371, -16478904, 16739175, 285431, 2763829, 15736322, 4143876),
            ),
            array(
                array(2379352, 11839345, -4110402, -5988665, 11274298, 794957, 212801, -14594663, 23527084, -16458268),
                array(33431127, -11130478, -17838966, -15626900, 8909499, 8376530, -32625340, 4087881, -15188911, -14416214),
                array(1767683, 7197987, -13205226, -2022635, -13091350, 448826, 5799055, 4357868, -4774191, -16323038),
            ),
        ),
        array(
            array(
                array(6721966, 13833823, -23523388, -1551314, 26354293, -11863321, 23365147, -3949732, 7390890, 2759800),
                array(4409041, 2052381, 23373853, 10530217, 7676779, -12885954, 21302353, -4264057, 1244380, -12919645),
                array(-4421239, 7169619, 4982368, -2957590, 30256825, -2777540, 14086413, 9208236, 15886429, 16489664),
            ),
            array(
                array(1996075, 10375649, 14346367, 13311202, -6874135, -16438411, -13693198, 398369, -30606455, -712933),
                array(-25307465, 9795880, -2777414, 14878809, -33531835, 14780363, 13348553, 12076947, -30836462, 5113182),
                array(-17770784, 11797796, 31950843, 13929123, -25888302, 12288344, -30341101, -7336386, 13847711, 5387222),
            ),
            array(
                array(-18582163, -3416217, 17824843, -2340966, 22744343, -10442611, 8763061, 3617786, -19600662, 10370991),
                array(20246567, -14369378, 22358229, -543712, 18507283, -10413996, 14554437, -8746092, 32232924, 16763880),
                array(9648505, 10094563, 26416693, 14745928, -30374318, -6472621, 11094161, 15689506, 3140038, -16510092),
            ),
            array(
                array(-16160072, 5472695, 31895588, 4744994, 8823515, 10365685, -27224800, 9448613, -28774454, 366295),
                array(19153450, 11523972, -11096490, -6503142, -24647631, 5420647, 28344573, 8041113, 719605, 11671788),
                array(8678025, 2694440, -6808014, 2517372, 4964326, 11152271, -15432916, -15266516, 27000813, -10195553),
            ),
            array(
                array(-15157904, 7134312, 8639287, -2814877, -7235688, 10421742, 564065, 5336097, 6750977, -14521026),
                array(11836410, -3979488, 26297894, 16080799, 23455045, 15735944, 1695823, -8819122, 8169720, 16220347),
                array(-18115838, 8653647, 17578566, -6092619, -8025777, -16012763, -11144307, -2627664, -5990708, -14166033),
            ),
            array(
                array(-23308498, -10968312, 15213228, -10081214, -30853605, -11050004, 27884329, 2847284, 2655861, 1738395),
                array(-27537433, -14253021, -25336301, -8002780, -9370762, 8129821, 21651608, -3239336, -19087449, -11005278),
                array(1533110, 3437855, 23735889, 459276, 29970501, 11335377, 26030092, 5821408, 10478196, 8544890),
            ),
            array(
                array(32173121, -16129311, 24896207, 3921497, 22579056, -3410854, 19270449, 12217473, 17789017, -3395995),
                array(-30552961, -2228401, -15578829, -10147201, 13243889, 517024, 15479401, -3853233, 30460520, 1052596),
                array(-11614875, 13323618, 32618793, 8175907, -15230173, 12596687, 27491595, -4612359, 3179268, -9478891),
            ),
            array(
                array(31947069, -14366651, -4640583, -15339921, -15125977, -6039709, -14756777, -16411740, 19072640, -9511060),
                array(11685058, 11822410, 3158003, -13952594, 33402194, -4165066, 5977896, -5215017, 473099, 5040608),
                array(-20290863, 8198642, -27410132, 11602123, 1290375, -2799760, 28326862, 1721092, -19558642, -3131606),
            ),
        ),
        array(
            array(
                array(7881532, 10687937, 7578723, 7738378, -18951012, -2553952, 21820786, 8076149, -27868496, 11538389),
                array(-19935666, 3899861, 18283497, -6801568, -15728660, -11249211, 8754525, 7446702, -5676054, 5797016),
                array(-11295600, -3793569, -15782110, -7964573, 12708869, -8456199, 2014099, -9050574, -2369172, -5877341),
            ),
            array(
                array(-22472376, -11568741, -27682020, 1146375, 18956691, 16640559, 1192730, -3714199, 15123619, 10811505),
                array(14352098, -3419715, -18942044, 10822655, 32750596, 4699007, -70363, 15776356, -28886779, -11974553),
                array(-28241164, -8072475, -4978962, -5315317, 29416931, 1847569, -20654173, -16484855, 4714547, -9600655),
            ),
            array(
                array(15200332, 8368572, 19679101, 15970074, -31872674, 1959451, 24611599, -4543832, -11745876, 12340220),
                array(12876937, -10480056, 33134381, 6590940, -6307776, 14872440, 9613953, 8241152, 15370987, 9608631),
                array(-4143277, -12014408, 8446281, -391603, 4407738, 13629032, -7724868, 15866074, -28210621, -8814099),
            ),
            array(
                array(26660628, -15677655, 8393734, 358047, -7401291, 992988, -23904233, 858697, 20571223, 8420556),
                array(14620715, 13067227, -15447274, 8264467, 14106269, 15080814, 33531827, 12516406, -21574435, -12476749),
                array(236881, 10476226, 57258, -14677024, 6472998, 2466984, 17258519, 7256740, 8791136, 15069930),
            ),
            array(
                array(1276410, -9371918, 22949635, -16322807, -23493039, -5702186, 14711875, 4874229, -30663140, -2331391),
                array(5855666, 4990204, -13711848, 7294284, -7804282, 1924647, -1423175, -7912378, -33069337, 9234253),
                array(20590503, -9018988, 31529744, -7352666, -2706834, 10650548, 31559055, -11609587, 18979186, 13396066),
            ),
            array(
                array(24474287, 4968103, 22267082, 4407354, 24063882, -8325180, -18816887, 13594782, 33514650, 7021958),
                array(-11566906, -6565505, -21365085, 15928892, -26158305, 4315421, -25948728, -3916677, -21480480, 12868082),
                array(-28635013, 13504661, 19988037, -2132761, 21078225, 6443208, -21446107, 2244500, -12455797, -8089383),
            ),
            array(
                array(-30595528, 13793479, -5852820, 319136, -25723172, -6263899, 33086546, 8957937, -15233648, 5540521),
                array(-11630176, -11503902, -8119500, -7643073, 2620056, 1022908, -23710744, -1568984, -16128528, -14962807),
                array(23152971, 775386, 27395463, 14006635, -9701118, 4649512, 1689819, 892185, -11513277, -15205948),
            ),
            array(
                array(9770129, 9586738, 26496094, 4324120, 1556511, -3550024, 27453819, 4763127, -19179614, 5867134),
                array(-32765025, 1927590, 31726409, -4753295, 23962434, -16019500, 27846559, 5931263, -29749703, -16108455),
                array(27461885, -2977536, 22380810, 1815854, -23033753, -3031938, 7283490, -15148073, -19526700, 7734629),
            ),
        ),
        array(
            array(
                array(-8010264, -9590817, -11120403, 6196038, 29344158, -13430885, 7585295, -3176626, 18549497, 15302069),
                array(-32658337, -6171222, -7672793, -11051681, 6258878, 13504381, 10458790, -6418461, -8872242, 8424746),
                array(24687205, 8613276, -30667046, -3233545, 1863892, -1830544, 19206234, 7134917, -11284482, -828919),
            ),
            array(
                array(11334899, -9218022, 8025293, 12707519, 17523892, -10476071, 10243738, -14685461, -5066034, 16498837),
                array(8911542, 6887158, -9584260, -6958590, 11145641, -9543680, 17303925, -14124238, 6536641, 10543906),
                array(-28946384, 15479763, -17466835, 568876, -1497683, 11223454, -2669190, -16625574, -27235709, 8876771),
            ),
            array(
                array(-25742899, -12566864, -15649966, -846607, -33026686, -796288, -33481822, 15824474, -604426, -9039817),
                array(10330056, 70051, 7957388, -9002667, 9764902, 15609756, 27698697, -4890037, 1657394, 3084098),
                array(10477963, -7470260, 12119566, -13250805, 29016247, -5365589, 31280319, 14396151, -30233575, 15272409),
            ),
            array(
                array(-12288309, 3169463, 28813183, 16658753, 25116432, -5630466, -25173957, -12636138, -25014757, 1950504),
                array(-26180358, 9489187, 11053416, -14746161, -31053720, 5825630, -8384306, -8767532, 15341279, 8373727),
                array(28685821, 7759505, -14378516, -12002860, -31971820, 4079242, 298136, -10232602, -2878207, 15190420),
            ),
            array(
                array(-32932876, 13806336, -14337485, -15794431, -24004620, 10940928, 8669718, 2742393, -26033313, -6875003),
                array(-1580388, -11729417, -25979658, -11445023, -17411874, -10912854, 9291594, -16247779, -12154742, 6048605),
                array(-30305315, 14843444, 1539301, 11864366, 20201677, 1900163, 13934231, 5128323, 11213262, 9168384),
            ),
            array(
                array(-26280513, 11007847, 19408960, -940758, -18592965, -4328580, -5088060, -11105150, 20470157, -16398701),
                array(-23136053, 9282192, 14855179, -15390078, -7362815, -14408560, -22783952, 14461608, 14042978, 5230683),
                array(29969567, -2741594, -16711867, -8552442, 9175486, -2468974, 21556951, 3506042, -5933891, -12449708),
            ),
            array(
                array(-3144746, 8744661, 19704003, 4581278, -20430686, 6830683, -21284170, 8971513, -28539189, 15326563),
                array(-19464629, 10110288, -17262528, -3503892, -23500387, 1355669, -15523050, 15300988, -20514118, 9168260),
                array(-5353335, 4488613, -23803248, 16314347, 7780487, -15638939, -28948358, 9601605, 33087103, -9011387),
            ),
            array(
                array(-19443170, -15512900, -20797467, -12445323, -29824447, 10229461, -27444329, -15000531, -5996870, 15664672),
                array(23294591, -16632613, -22650781, -8470978, 27844204, 11461195, 13099750, -2460356, 18151676, 13417686),
                array(-24722913, -4176517, -31150679, 5988919, -26858785, 6685065, 1661597, -12551441, 15271676, -15452665),
            ),
        ),
        array(
            array(
                array(11433042, -13228665, 8239631, -5279517, -1985436, -725718, -18698764, 2167544, -6921301, -13440182),
                array(-31436171, 15575146, 30436815, 12192228, -22463353, 9395379, -9917708, -8638997, 12215110, 12028277),
                array(14098400, 6555944, 23007258, 5757252, -15427832, -12950502, 30123440, 4617780, -16900089, -655628),
            ),
            array(
                array(-4026201, -15240835, 11893168, 13718664, -14809462, 1847385, -15819999, 10154009, 23973261, -12684474),
                array(-26531820, -3695990, -1908898, 2534301, -31870557, -16550355, 18341390, -11419951, 32013174, -10103539),
                array(-25479301, 10876443, -11771086, -14625140, -12369567, 1838104, 21911214, 6354752, 4425632, -837822),
            ),
            array(
                array(-10433389, -14612966, 22229858, -3091047, -13191166, 776729, -17415375, -12020462, 4725005, 14044970),
                array(19268650, -7304421, 1555349, 8692754, -21474059, -9910664, 6347390, -1411784, -19522291, -16109756),
                array(-24864089, 12986008, -10898878, -5558584, -11312371, -148526, 19541418, 8180106, 9282262, 10282508),
            ),
            array(
                array(-26205082, 4428547, -8661196, -13194263, 4098402, -14165257, 15522535, 8372215, 5542595, -10702683),
                array(-10562541, 14895633, 26814552, -16673850, -17480754, -2489360, -2781891, 6993761, -18093885, 10114655),
                array(-20107055, -929418, 31422704, 10427861, -7110749, 6150669, -29091755, -11529146, 25953725, -106158),
            ),
            array(
                array(-4234397, -8039292, -9119125, 3046000, 2101609, -12607294, 19390020, 6094296, -3315279, 12831125),
                array(-15998678, 7578152, 5310217, 14408357, -33548620, -224739, 31575954, 6326196, 7381791, -2421839),
                array(-20902779, 3296811, 24736065, -16328389, 18374254, 7318640, 6295303, 8082724, -15362489, 12339664),
            ),
            array(
                array(27724736, 2291157, 6088201, -14184798, 1792727, 5857634, 13848414, 15768922, 25091167, 14856294),
                array(-18866652, 8331043, 24373479, 8541013, -701998, -9269457, 12927300, -12695493, -22182473, -9012899),
                array(-11423429, -5421590, 11632845, 3405020, 30536730, -11674039, -27260765, 13866390, 30146206, 9142070),
            ),
            array(
                array(3924129, -15307516, -13817122, -10054960, 12291820, -668366, -27702774, 9326384, -8237858, 4171294),
                array(-15921940, 16037937, 6713787, 16606682, -21612135, 2790944, 26396185, 3731949, 345228, -5462949),
                array(-21327538, 13448259, 25284571, 1143661, 20614966, -8849387, 2031539, -12391231, -16253183, -13582083),
            ),
            array(
                array(31016211, -16722429, 26371392, -14451233, -5027349, 14854137, 17477601, 3842657, 28012650, -16405420),
                array(-5075835, 9368966, -8562079, -4600902, -15249953, 6970560, -9189873, 16292057, -8867157, 3507940),
                array(29439664, 3537914, 23333589, 6997794, -17555561, -11018068, -15209202, -15051267, -9164929, 6580396),
            ),
        ),
        array(
            array(
                array(-12185861, -7679788, 16438269, 10826160, -8696817, -6235611, 17860444, -9273846, -2095802, 9304567),
                array(20714564, -4336911, 29088195, 7406487, 11426967, -5095705, 14792667, -14608617, 5289421, -477127),
                array(-16665533, -10650790, -6160345, -13305760, 9192020, -1802462, 17271490, 12349094, 26939669, -3752294),
            ),
            array(
                array(-12889898, 9373458, 31595848, 16374215, 21471720, 13221525, -27283495, -12348559, -3698806, 117887),
                array(22263325, -6560050, 3984570, -11174646, -15114008, -566785, 28311253, 5358056, -23319780, 541964),
                array(16259219, 3261970, 2309254, -15534474, -16885711, -4581916, 24134070, -16705829, -13337066, -13552195),
            ),
            array(
                array(9378160, -13140186, -22845982, -12745264, 28198281, -7244098, -2399684, -717351, 690426, 14876244),
                array(24977353, -314384, -8223969, -13465086, 28432343, -1176353, -13068804, -12297348, -22380984, 6618999),
                array(-1538174, 11685646, 12944378, 13682314, -24389511, -14413193, 8044829, -13817328, 32239829, -5652762),
            ),
            array(
                array(-18603066, 4762990, -926250, 8885304, -28412480, -3187315, 9781647, -10350059, 32779359, 5095274),
                array(-33008130, -5214506, -32264887, -3685216, 9460461, -9327423, -24601656, 14506724, 21639561, -2630236),
                array(-16400943, -13112215, 25239338, 15531969, 3987758, -4499318, -1289502, -6863535, 17874574, 558605),
            ),
            array(
                array(-13600129, 10240081, 9171883, 16131053, -20869254, 9599700, 33499487, 5080151, 2085892, 5119761),
                array(-22205145, -2519528, -16381601, 414691, -25019550, 2170430, 30634760, -8363614, -31999993, -5759884),
                array(-6845704, 15791202, 8550074, -1312654, 29928809, -12092256, 27534430, -7192145, -22351378, 12961482),
            ),
            array(
                array(-24492060, -9570771, 10368194, 11582341, -23397293, -2245287, 16533930, 8206996, -30194652, -5159638),
                array(-11121496, -3382234, 2307366, 6362031, -135455, 8868177, -16835630, 7031275, 7589640, 8945490),
                array(-32152748, 8917967, 6661220, -11677616, -1192060, -15793393, 7251489, -11182180, 24099109, -14456170),
            ),
            array(
                array(5019558, -7907470, 4244127, -14714356, -26933272, 6453165, -19118182, -13289025, -6231896, -10280736),
                array(10853594, 10721687, 26480089, 5861829, -22995819, 1972175, -1866647, -10557898, -3363451, -6441124),
                array(-17002408, 5906790, 221599, -6563147, 7828208, -13248918, 24362661, -2008168, -13866408, 7421392),
            ),
            array(
                array(8139927, -6546497, 32257646, -5890546, 30375719, 1886181, -21175108, 15441252, 28826358, -4123029),
                array(6267086, 9695052, 7709135, -16603597, -32869068, -1886135, 14795160, -7840124, 13746021, -1742048),
                array(28584902, 7787108, -6732942, -15050729, 22846041, -7571236, -3181936, -363524, 4771362, -8419958),
            ),
        ),
        array(
            array(
                array(24949256, 6376279, -27466481, -8174608, -18646154, -9930606, 33543569, -12141695, 3569627, 11342593),
                array(26514989, 4740088, 27912651, 3697550, 19331575, -11472339, 6809886, 4608608, 7325975, -14801071),
                array(-11618399, -14554430, -24321212, 7655128, -1369274, 5214312, -27400540, 10258390, -17646694, -8186692),
            ),
            array(
                array(11431204, 15823007, 26570245, 14329124, 18029990, 4796082, -31446179, 15580664, 9280358, -3973687),
                array(-160783, -10326257, -22855316, -4304997, -20861367, -13621002, -32810901, -11181622, -15545091, 4387441),
                array(-20799378, 12194512, 3937617, -5805892, -27154820, 9340370, -24513992, 8548137, 20617071, -7482001),
            ),
            array(
                array(-938825, -3930586, -8714311, 16124718, 24603125, -6225393, -13775352, -11875822, 24345683, 10325460),
                array(-19855277, -1568885, -22202708, 8714034, 14007766, 6928528, 16318175, -1010689, 4766743, 3552007),
                array(-21751364, -16730916, 1351763, -803421, -4009670, 3950935, 3217514, 14481909, 10988822, -3994762),
            ),
            array(
                array(15564307, -14311570, 3101243, 5684148, 30446780, -8051356, 12677127, -6505343, -8295852, 13296005),
                array(-9442290, 6624296, -30298964, -11913677, -4670981, -2057379, 31521204, 9614054, -30000824, 12074674),
                array(4771191, -135239, 14290749, -13089852, 27992298, 14998318, -1413936, -1556716, 29832613, -16391035),
            ),
            array(
                array(7064884, -7541174, -19161962, -5067537, -18891269, -2912736, 25825242, 5293297, -27122660, 13101590),
                array(-2298563, 2439670, -7466610, 1719965, -27267541, -16328445, 32512469, -5317593, -30356070, -4190957),
                array(-30006540, 10162316, -33180176, 3981723, -16482138, -13070044, 14413974, 9515896, 19568978, 9628812),
            ),
            array(
                array(33053803, 199357, 15894591, 1583059, 27380243, -4580435, -17838894, -6106839, -6291786, 3437740),
                array(-18978877, 3884493, 19469877, 12726490, 15913552, 13614290, -22961733, 70104, 7463304, 4176122),
                array(-27124001, 10659917, 11482427, -16070381, 12771467, -6635117, -32719404, -5322751, 24216882, 5944158),
            ),
            array(
                array(8894125, 7450974, -2664149, -9765752, -28080517, -12389115, 19345746, 14680796, 11632993, 5847885),
                array(26942781, -2315317, 9129564, -4906607, 26024105, 11769399, -11518837, 6367194, -9727230, 4782140),
                array(19916461, -4828410, -22910704, -11414391, 25606324, -5972441, 33253853, 8220911, 6358847, -1873857),
            ),
            array(
                array(801428, -2081702, 16569428, 11065167, 29875704, 96627, 7908388, -4480480, -13538503, 1387155),
                array(19646058, 5720633, -11416706, 12814209, 11607948, 12749789, 14147075, 15156355, -21866831, 11835260),
                array(19299512, 1155910, 28703737, 14890794, 2925026, 7269399, 26121523, 15467869, -26560550, 5052483),
            ),
        ),
        array(
            array(
                array(-3017432, 10058206, 1980837, 3964243, 22160966, 12322533, -6431123, -12618185, 12228557, -7003677),
                array(32944382, 14922211, -22844894, 5188528, 21913450, -8719943, 4001465, 13238564, -6114803, 8653815),
                array(22865569, -4652735, 27603668, -12545395, 14348958, 8234005, 24808405, 5719875, 28483275, 2841751),
            ),
            array(
                array(-16420968, -1113305, -327719, -12107856, 21886282, -15552774, -1887966, -315658, 19932058, -12739203),
                array(-11656086, 10087521, -8864888, -5536143, -19278573, -3055912, 3999228, 13239134, -4777469, -13910208),
                array(1382174, -11694719, 17266790, 9194690, -13324356, 9720081, 20403944, 11284705, -14013818, 3093230),
            ),
            array(
                array(16650921, -11037932, -1064178, 1570629, -8329746, 7352753, -302424, 16271225, -24049421, -6691850),
                array(-21911077, -5927941, -4611316, -5560156, -31744103, -10785293, 24123614, 15193618, -21652117, -16739389),
                array(-9935934, -4289447, -25279823, 4372842, 2087473, 10399484, 31870908, 14690798, 17361620, 11864968),
            ),
            array(
                array(-11307610, 6210372, 13206574, 5806320, -29017692, -13967200, -12331205, -7486601, -25578460, -16240689),
                array(14668462, -12270235, 26039039, 15305210, 25515617, 4542480, 10453892, 6577524, 9145645, -6443880),
                array(5974874, 3053895, -9433049, -10385191, -31865124, 3225009, -7972642, 3936128, -5652273, -3050304),
            ),
            array(
                array(30625386, -4729400, -25555961, -12792866, -20484575, 7695099, 17097188, -16303496, -27999779, 1803632),
                array(-3553091, 9865099, -5228566, 4272701, -5673832, -16689700, 14911344, 12196514, -21405489, 7047412),
                array(20093277, 9920966, -11138194, -5343857, 13161587, 12044805, -32856851, 4124601, -32343828, -10257566),
            ),
            array(
                array(-20788824, 14084654, -13531713, 7842147, 19119038, -13822605, 4752377, -8714640, -21679658, 2288038),
                array(-26819236, -3283715, 29965059, 3039786, -14473765, 2540457, 29457502, 14625692, -24819617, 12570232),
                array(-1063558, -11551823, 16920318, 12494842, 1278292, -5869109, -21159943, -3498680, -11974704, 4724943),
            ),
            array(
                array(17960970, -11775534, -4140968, -9702530, -8876562, -1410617, -12907383, -8659932, -29576300, 1903856),
                array(23134274, -14279132, -10681997, -1611936, 20684485, 15770816, -12989750, 3190296, 26955097, 14109738),
                array(15308788, 5320727, -30113809, -14318877, 22902008, 7767164, 29425325, -11277562, 31960942, 11934971),
            ),
            array(
                array(-27395711, 8435796, 4109644, 12222639, -24627868, 14818669, 20638173, 4875028, 10491392, 1379718),
                array(-13159415, 9197841, 3875503, -8936108, -1383712, -5879801, 33518459, 16176658, 21432314, 12180697),
                array(-11787308, 11500838, 13787581, -13832590, -22430679, 10140205, 1465425, 12689540, -10301319, -13872883),
            ),
        ),
        array(
            array(
                array(5414091, -15386041, -21007664, 9643570, 12834970, 1186149, -2622916, -1342231, 26128231, 6032912),
                array(-26337395, -13766162, 32496025, -13653919, 17847801, -12669156, 3604025, 8316894, -25875034, -10437358),
                array(3296484, 6223048, 24680646, -12246460, -23052020, 5903205, -8862297, -4639164, 12376617, 3188849),
            ),
            array(
                array(29190488, -14659046, 27549113, -1183516, 3520066, -10697301, 32049515, -7309113, -16109234, -9852307),
                array(-14744486, -9309156, 735818, -598978, -20407687, -5057904, 25246078, -15795669, 18640741, -960977),
                array(-6928835, -16430795, 10361374, 5642961, 4910474, 12345252, -31638386, -494430, 10530747, 1053335),
            ),
            array(
                array(-29265967, -14186805, -13538216, -12117373, -19457059, -10655384, -31462369, -2948985, 24018831, 15026644),
                array(-22592535, -3145277, -2289276, 5953843, -13440189, 9425631, 25310643, 13003497, -2314791, -15145616),
                array(-27419985, -603321, -8043984, -1669117, -26092265, 13987819, -27297622, 187899, -23166419, -2531735),
            ),
            array(
                array(-21744398, -13810475, 1844840, 5021428, -10434399, -15911473, 9716667, 16266922, -5070217, 726099),
                array(29370922, -6053998, 7334071, -15342259, 9385287, 2247707, -13661962, -4839461, 30007388, -15823341),
                array(-936379, 16086691, 23751945, -543318, -1167538, -5189036, 9137109, 730663, 9835848, 4555336),
            ),
            array(
                array(-23376435, 1410446, -22253753, -12899614, 30867635, 15826977, 17693930, 544696, -11985298, 12422646),
                array(31117226, -12215734, -13502838, 6561947, -9876867, -12757670, -5118685, -4096706, 29120153, 13924425),
                array(-17400879, -14233209, 19675799, -2734756, -11006962, -5858820, -9383939, -11317700, 7240931, -237388),
            ),
            array(
                array(-31361739, -11346780, -15007447, -5856218, -22453340, -12152771, 1222336, 4389483, 3293637, -15551743),
                array(-16684801, -14444245, 11038544, 11054958, -13801175, -3338533, -24319580, 7733547, 12796905, -6335822),
                array(-8759414, -10817836, -25418864, 10783769, -30615557, -9746811, -28253339, 3647836, 3222231, -11160462),
            ),
            array(
                array(18606113, 1693100, -25448386, -15170272, 4112353, 10045021, 23603893, -2048234, -7550776, 2484985),
                array(9255317, -3131197, -12156162, -1004256, 13098013, -9214866, 16377220, -2102812, -19802075, -3034702),
                array(-22729289, 7496160, -5742199, 11329249, 19991973, -3347502, -31718148, 9936966, -30097688, -10618797),
            ),
            array(
                array(21878590, -5001297, 4338336, 13643897, -3036865, 13160960, 19708896, 5415497, -7360503, -4109293),
                array(27736861, 10103576, 12500508, 8502413, -3413016, -9633558, 10436918, -1550276, -23659143, -8132100),
                array(19492550, -12104365, -29681976, -852630, -3208171, 12403437, 30066266, 8367329, 13243957, 8709688),
            ),
        ),
        array(
            array(
                array(12015105, 2801261, 28198131, 10151021, 24818120, -4743133, -11194191, -5645734, 5150968, 7274186),
                array(2831366, -12492146, 1478975, 6122054, 23825128, -12733586, 31097299, 6083058, 31021603, -9793610),
                array(-2529932, -2229646, 445613, 10720828, -13849527, -11505937, -23507731, 16354465, 15067285, -14147707),
            ),
            array(
                array(7840942, 14037873, -33364863, 15934016, -728213, -3642706, 21403988, 1057586, -19379462, -12403220),
                array(915865, -16469274, 15608285, -8789130, -24357026, 6060030, -17371319, 8410997, -7220461, 16527025),
                array(32922597, -556987, 20336074, -16184568, 10903705, -5384487, 16957574, 52992, 23834301, 6588044),
            ),
            array(
                array(32752030, 11232950, 3381995, -8714866, 22652988, -10744103, 17159699, 16689107, -20314580, -1305992),
                array(-4689649, 9166776, -25710296, -10847306, 11576752, 12733943, 7924251, -2752281, 1976123, -7249027),
                array(21251222, 16309901, -2983015, -6783122, 30810597, 12967303, 156041, -3371252, 12331345, -8237197),
            ),
            array(
                array(8651614, -4477032, -16085636, -4996994, 13002507, 2950805, 29054427, -5106970, 10008136, -4667901),
                array(31486080, 15114593, -14261250, 12951354, 14369431, -7387845, 16347321, -13662089, 8684155, -10532952),
                array(19443825, 11385320, 24468943, -9659068, -23919258, 2187569, -26263207, -6086921, 31316348, 14219878),
            ),
            array(
                array(-28594490, 1193785, 32245219, 11392485, 31092169, 15722801, 27146014, 6992409, 29126555, 9207390),
                array(32382935, 1110093, 18477781, 11028262, -27411763, -7548111, -4980517, 10843782, -7957600, -14435730),
                array(2814918, 7836403, 27519878, -7868156, -20894015, -11553689, -21494559, 8550130, 28346258, 1994730),
            ),
            array(
                array(-19578299, 8085545, -14000519, -3948622, 2785838, -16231307, -19516951, 7174894, 22628102, 8115180),
                array(-30405132, 955511, -11133838, -15078069, -32447087, -13278079, -25651578, 3317160, -9943017, 930272),
                array(-15303681, -6833769, 28856490, 1357446, 23421993, 1057177, 24091212, -1388970, -22765376, -10650715),
            ),
            array(
                array(-22751231, -5303997, -12907607, -12768866, -15811511, -7797053, -14839018, -16554220, -1867018, 8398970),
                array(-31969310, 2106403, -4736360, 1362501, 12813763, 16200670, 22981545, -6291273, 18009408, -15772772),
                array(-17220923, -9545221, -27784654, 14166835, 29815394, 7444469, 29551787, -3727419, 19288549, 1325865),
            ),
            array(
                array(15100157, -15835752, -23923978, -1005098, -26450192, 15509408, 12376730, -3479146, 33166107, -8042750),
                array(20909231, 13023121, -9209752, 16251778, -5778415, -8094914, 12412151, 10018715, 2213263, -13878373),
                array(32529814, -11074689, 30361439, -16689753, -9135940, 1513226, 22922121, 6382134, -5766928, 8371348),
            ),
        ),
        array(
            array(
                array(9923462, 11271500, 12616794, 3544722, -29998368, -1721626, 12891687, -8193132, -26442943, 10486144),
                array(-22597207, -7012665, 8587003, -8257861, 4084309, -12970062, 361726, 2610596, -23921530, -11455195),
                array(5408411, -1136691, -4969122, 10561668, 24145918, 14240566, 31319731, -4235541, 19985175, -3436086),
            ),
            array(
                array(-13994457, 16616821, 14549246, 3341099, 32155958, 13648976, -17577068, 8849297, 65030, 8370684),
                array(-8320926, -12049626, 31204563, 5839400, -20627288, -1057277, -19442942, 6922164, 12743482, -9800518),
                array(-2361371, 12678785, 28815050, 4759974, -23893047, 4884717, 23783145, 11038569, 18800704, 255233),
            ),
            array(
                array(-5269658, -1773886, 13957886, 7990715, 23132995, 728773, 13393847, 9066957, 19258688, -14753793),
                array(-2936654, -10827535, -10432089, 14516793, -3640786, 4372541, -31934921, 2209390, -1524053, 2055794),
                array(580882, 16705327, 5468415, -2683018, -30926419, -14696000, -7203346, -8994389, -30021019, 7394435),
            ),
            array(
                array(23838809, 1822728, -15738443, 15242727, 8318092, -3733104, -21672180, -3492205, -4821741, 14799921),
                array(13345610, 9759151, 3371034, -16137791, 16353039, 8577942, 31129804, 13496856, -9056018, 7402518),
                array(2286874, -4435931, -20042458, -2008336, -13696227, 5038122, 11006906, -15760352, 8205061, 1607563),
            ),
            array(
                array(14414086, -8002132, 3331830, -3208217, 22249151, -5594188, 18364661, -2906958, 30019587, -9029278),
                array(-27688051, 1585953, -10775053, 931069, -29120221, -11002319, -14410829, 12029093, 9944378, 8024),
                array(4368715, -3709630, 29874200, -15022983, -20230386, -11410704, -16114594, -999085, -8142388, 5640030),
            ),
            array(
                array(10299610, 13746483, 11661824, 16234854, 7630238, 5998374, 9809887, -16694564, 15219798, -14327783),
                array(27425505, -5719081, 3055006, 10660664, 23458024, 595578, -15398605, -1173195, -18342183, 9742717),
                array(6744077, 2427284, 26042789, 2720740, -847906, 1118974, 32324614, 7406442, 12420155, 1994844),
            ),
            array(
                array(14012521, -5024720, -18384453, -9578469, -26485342, -3936439, -13033478, -10909803, 24319929, -6446333),
                array(16412690, -4507367, 10772641, 15929391, -17068788, -4658621, 10555945, -10484049, -30102368, -4739048),
                array(22397382, -7767684, -9293161, -12792868, 17166287, -9755136, -27333065, 6199366, 21880021, -12250760),
            ),
            array(
                array(-4283307, 5368523, -31117018, 8163389, -30323063, 3209128, 16557151, 8890729, 8840445, 4957760),
                array(-15447727, 709327, -6919446, -10870178, -29777922, 6522332, -21720181, 12130072, -14796503, 5005757),
                array(-2114751, -14308128, 23019042, 15765735, -25269683, 6002752, 10183197, -13239326, -16395286, -2176112),
            ),
        ),
        array(
            array(
                array(-19025756, 1632005, 13466291, -7995100, -23640451, 16573537, -32013908, -3057104, 22208662, 2000468),
                array(3065073, -1412761, -25598674, -361432, -17683065, -5703415, -8164212, 11248527, -3691214, -7414184),
                array(10379208, -6045554, 8877319, 1473647, -29291284, -12507580, 16690915, 2553332, -3132688, 16400289),
            ),
            array(
                array(15716668, 1254266, -18472690, 7446274, -8448918, 6344164, -22097271, -7285580, 26894937, 9132066),
                array(24158887, 12938817, 11085297, -8177598, -28063478, -4457083, -30576463, 64452, -6817084, -2692882),
                array(13488534, 7794716, 22236231, 5989356, 25426474, -12578208, 2350710, -3418511, -4688006, 2364226),
            ),
            array(
                array(16335052, 9132434, 25640582, 6678888, 1725628, 8517937, -11807024, -11697457, 15445875, -7798101),
                array(29004207, -7867081, 28661402, -640412, -12794003, -7943086, 31863255, -4135540, -278050, -15759279),
                array(-6122061, -14866665, -28614905, 14569919, -10857999, -3591829, 10343412, -6976290, -29828287, -10815811),
            ),
            array(
                array(27081650, 3463984, 14099042, -4517604, 1616303, -6205604, 29542636, 15372179, 17293797, 960709),
                array(20263915, 11434237, -5765435, 11236810, 13505955, -10857102, -16111345, 6493122, -19384511, 7639714),
                array(-2830798, -14839232, 25403038, -8215196, -8317012, -16173699, 18006287, -16043750, 29994677, -15808121),
            ),
            array(
                array(9769828, 5202651, -24157398, -13631392, -28051003, -11561624, -24613141, -13860782, -31184575, 709464),
                array(12286395, 13076066, -21775189, -1176622, -25003198, 4057652, -32018128, -8890874, 16102007, 13205847),
                array(13733362, 5599946, 10557076, 3195751, -5557991, 8536970, -25540170, 8525972, 10151379, 10394400),
            ),
            array(
                array(4024660, -16137551, 22436262, 12276534, -9099015, -2686099, 19698229, 11743039, -33302334, 8934414),
                array(-15879800, -4525240, -8580747, -2934061, 14634845, -698278, -9449077, 3137094, -11536886, 11721158),
                array(17555939, -5013938, 8268606, 2331751, -22738815, 9761013, 9319229, 8835153, -9205489, -1280045),
            ),
            array(
                array(-461409, -7830014, 20614118, 16688288, -7514766, -4807119, 22300304, 505429, 6108462, -6183415),
                array(-5070281, 12367917, -30663534, 3234473, 32617080, -8422642, 29880583, -13483331, -26898490, -7867459),
                array(-31975283, 5726539, 26934134, 10237677, -3173717, -605053, 24199304, 3795095, 7592688, -14992079),
            ),
            array(
                array(21594432, -14964228, 17466408, -4077222, 32537084, 2739898, 6407723, 12018833, -28256052, 4298412),
                array(-20650503, -11961496, -27236275, 570498, 3767144, -1717540, 13891942, -1569194, 13717174, 10805743),
                array(-14676630, -15644296, 15287174, 11927123, 24177847, -8175568, -796431, 14860609, -26938930, -5863836),
            ),
        ),
        array(
            array(
                array(12962541, 5311799, -10060768, 11658280, 18855286, -7954201, 13286263, -12808704, -4381056, 9882022),
                array(18512079, 11319350, -20123124, 15090309, 18818594, 5271736, -22727904, 3666879, -23967430, -3299429),
                array(-6789020, -3146043, 16192429, 13241070, 15898607, -14206114, -10084880, -6661110, -2403099, 5276065),
            ),
            array(
                array(30169808, -5317648, 26306206, -11750859, 27814964, 7069267, 7152851, 3684982, 1449224, 13082861),
                array(10342826, 3098505, 2119311, 193222, 25702612, 12233820, 23697382, 15056736, -21016438, -8202000),
                array(-33150110, 3261608, 22745853, 7948688, 19370557, -15177665, -26171976, 6482814, -10300080, -11060101),
            ),
            array(
                array(32869458, -5408545, 25609743, 15678670, -10687769, -15471071, 26112421, 2521008, -22664288, 6904815),
                array(29506923, 4457497, 3377935, -9796444, -30510046, 12935080, 1561737, 3841096, -29003639, -6657642),
                array(10340844, -6630377, -18656632, -2278430, 12621151, -13339055, 30878497, -11824370, -25584551, 5181966),
            ),
            array(
                array(25940115, -12658025, 17324188, -10307374, -8671468, 15029094, 24396252, -16450922, -2322852, -12388574),
                array(-21765684, 9916823, -1300409, 4079498, -1028346, 11909559, 1782390, 12641087, 20603771, -6561742),
                array(-18882287, -11673380, 24849422, 11501709, 13161720, -4768874, 1925523, 11914390, 4662781, 7820689),
            ),
            array(
                array(12241050, -425982, 8132691, 9393934, 32846760, -1599620, 29749456, 12172924, 16136752, 15264020),
                array(-10349955, -14680563, -8211979, 2330220, -17662549, -14545780, 10658213, 6671822, 19012087, 3772772),
                array(3753511, -3421066, 10617074, 2028709, 14841030, -6721664, 28718732, -15762884, 20527771, 12988982),
            ),
            array(
                array(-14822485, -5797269, -3707987, 12689773, -898983, -10914866, -24183046, -10564943, 3299665, -12424953),
                array(-16777703, -15253301, -9642417, 4978983, 3308785, 8755439, 6943197, 6461331, -25583147, 8991218),
                array(-17226263, 1816362, -1673288, -6086439, 31783888, -8175991, -32948145, 7417950, -30242287, 1507265),
            ),
            array(
                array(29692663, 6829891, -10498800, 4334896, 20945975, -11906496, -28887608, 8209391, 14606362, -10647073),
                array(-3481570, 8707081, 32188102, 5672294, 22096700, 1711240, -33020695, 9761487, 4170404, -2085325),
                array(-11587470, 14855945, -4127778, -1531857, -26649089, 15084046, 22186522, 16002000, -14276837, -8400798),
            ),
            array(
                array(-4811456, 13761029, -31703877, -2483919, -3312471, 7869047, -7113572, -9620092, 13240845, 10965870),
                array(-7742563, -8256762, -14768334, -13656260, -23232383, 12387166, 4498947, 14147411, 29514390, 4302863),
                array(-13413405, -12407859, 20757302, -13801832, 14785143, 8976368, -5061276, -2144373, 17846988, -13971927),
            ),
        ),
        array(
            array(
                array(-2244452, -754728, -4597030, -1066309, -6247172, 1455299, -21647728, -9214789, -5222701, 12650267),
                array(-9906797, -16070310, 21134160, 12198166, -27064575, 708126, 387813, 13770293, -19134326, 10958663),
                array(22470984, 12369526, 23446014, -5441109, -21520802, -9698723, -11772496, -11574455, -25083830, 4271862),
            ),
            array(
                array(-25169565, -10053642, -19909332, 15361595, -5984358, 2159192, 75375, -4278529, -32526221, 8469673),
                array(15854970, 4148314, -8893890, 7259002, 11666551, 13824734, -30531198, 2697372, 24154791, -9460943),
                array(15446137, -15806644, 29759747, 14019369, 30811221, -9610191, -31582008, 12840104, 24913809, 9815020),
            ),
            array(
                array(-4709286, -5614269, -31841498, -12288893, -14443537, 10799414, -9103676, 13438769, 18735128, 9466238),
                array(11933045, 9281483, 5081055, -5183824, -2628162, -4905629, -7727821, -10896103, -22728655, 16199064),
                array(14576810, 379472, -26786533, -8317236, -29426508, -10812974, -102766, 1876699, 30801119, 2164795),
            ),
            array(
                array(15995086, 3199873, 13672555, 13712240, -19378835, -4647646, -13081610, -15496269, -13492807, 1268052),
                array(-10290614, -3659039, -3286592, 10948818, 23037027, 3794475, -3470338, -12600221, -17055369, 3565904),
                array(29210088, -9419337, -5919792, -4952785, 10834811, -13327726, -16512102, -10820713, -27162222, -14030531),
            ),
            array(
                array(-13161890, 15508588, 16663704, -8156150, -28349942, 9019123, -29183421, -3769423, 2244111, -14001979),
                array(-5152875, -3800936, -9306475, -6071583, 16243069, 14684434, -25673088, -16180800, 13491506, 4641841),
                array(10813417, 643330, -19188515, -728916, 30292062, -16600078, 27548447, -7721242, 14476989, -12767431),
            ),
            array(
                array(10292079, 9984945, 6481436, 8279905, -7251514, 7032743, 27282937, -1644259, -27912810, 12651324),
                array(-31185513, -813383, 22271204, 11835308, 10201545, 15351028, 17099662, 3988035, 21721536, -3148940),
                array(10202177, -6545839, -31373232, -9574638, -32150642, -8119683, -12906320, 3852694, 13216206, 14842320),
            ),
            array(
                array(-15815640, -10601066, -6538952, -7258995, -6984659, -6581778, -31500847, 13765824, -27434397, 9900184),
                array(14465505, -13833331, -32133984, -14738873, -27443187, 12990492, 33046193, 15796406, -7051866, -8040114),
                array(30924417, -8279620, 6359016, -12816335, 16508377, 9071735, -25488601, 15413635, 9524356, -7018878),
            ),
            array(
                array(12274201, -13175547, 32627641, -1785326, 6736625, 13267305, 5237659, -5109483, 15663516, 4035784),
                array(-2951309, 8903985, 17349946, 601635, -16432815, -4612556, -13732739, -15889334, -22258478, 4659091),
                array(-16916263, -4952973, -30393711, -15158821, 20774812, 15897498, 5736189, 15026997, -2178256, -13455585),
            ),
        ),
        array(
            array(
                array(-8858980, -2219056, 28571666, -10155518, -474467, -10105698, -3801496, 278095, 23440562, -290208),
                array(10226241, -5928702, 15139956, 120818, -14867693, 5218603, 32937275, 11551483, -16571960, -7442864),
                array(17932739, -12437276, -24039557, 10749060, 11316803, 7535897, 22503767, 5561594, -3646624, 3898661),
            ),
            array(
                array(7749907, -969567, -16339731, -16464, -25018111, 15122143, -1573531, 7152530, 21831162, 1245233),
                array(26958459, -14658026, 4314586, 8346991, -5677764, 11960072, -32589295, -620035, -30402091, -16716212),
                array(-12165896, 9166947, 33491384, 13673479, 29787085, 13096535, 6280834, 14587357, -22338025, 13987525),
            ),
            array(
                array(-24349909, 7778775, 21116000, 15572597, -4833266, -5357778, -4300898, -5124639, -7469781, -2858068),
                array(9681908, -6737123, -31951644, 13591838, -6883821, 386950, 31622781, 6439245, -14581012, 4091397),
                array(-8426427, 1470727, -28109679, -1596990, 3978627, -5123623, -19622683, 12092163, 29077877, -14741988),
            ),
            array(
                array(5269168, -6859726, -13230211, -8020715, 25932563, 1763552, -5606110, -5505881, -20017847, 2357889),
                array(32264008, -15407652, -5387735, -1160093, -2091322, -3946900, 23104804, -12869908, 5727338, 189038),
                array(14609123, -8954470, -6000566, -16622781, -14577387, -7743898, -26745169, 10942115, -25888931, -14884697),
            ),
            array(
                array(20513500, 5557931, -15604613, 7829531, 26413943, -2019404, -21378968, 7471781, 13913677, -5137875),
                array(-25574376, 11967826, 29233242, 12948236, -6754465, 4713227, -8940970, 14059180, 12878652, 8511905),
                array(-25656801, 3393631, -2955415, -7075526, -2250709, 9366908, -30223418, 6812974, 5568676, -3127656),
            ),
            array(
                array(11630004, 12144454, 2116339, 13606037, 27378885, 15676917, -17408753, -13504373, -14395196, 8070818),
                array(27117696, -10007378, -31282771, -5570088, 1127282, 12772488, -29845906, 10483306, -11552749, -1028714),
                array(10637467, -5688064, 5674781, 1072708, -26343588, -6982302, -1683975, 9177853, -27493162, 15431203),
            ),
            array(
                array(20525145, 10892566, -12742472, 12779443, -29493034, 16150075, -28240519, 14943142, -15056790, -7935931),
                array(-30024462, 5626926, -551567, -9981087, 753598, 11981191, 25244767, -3239766, -3356550, 9594024),
                array(-23752644, 2636870, -5163910, -10103818, 585134, 7877383, 11345683, -6492290, 13352335, -10977084),
            ),
            array(
                array(-1931799, -5407458, 3304649, -12884869, 17015806, -4877091, -29783850, -7752482, -13215537, -319204),
                array(20239939, 6607058, 6203985, 3483793, -18386976, -779229, -20723742, 15077870, -22750759, 14523817),
                array(27406042, -6041657, 27423596, -4497394, 4996214, 10002360, -28842031, -4545494, -30172742, -4805667),
            ),
        ),
        array(
            array(
                array(11374242, 12660715, 17861383, -12540833, 10935568, 1099227, -13886076, -9091740, -27727044, 11358504),
                array(-12730809, 10311867, 1510375, 10778093, -2119455, -9145702, 32676003, 11149336, -26123651, 4985768),
                array(-19096303, 341147, -6197485, -239033, 15756973, -8796662, -983043, 13794114, -19414307, -15621255),
            ),
            array(
                array(6490081, 11940286, 25495923, -7726360, 8668373, -8751316, 3367603, 6970005, -1691065, -9004790),
                array(1656497, 13457317, 15370807, 6364910, 13605745, 8362338, -19174622, -5475723, -16796596, -5031438),
                array(-22273315, -13524424, -64685, -4334223, -18605636, -10921968, -20571065, -7007978, -99853, -10237333),
            ),
            array(
                array(17747465, 10039260, 19368299, -4050591, -20630635, -16041286, 31992683, -15857976, -29260363, -5511971),
                array(31932027, -4986141, -19612382, 16366580, 22023614, 88450, 11371999, -3744247, 4882242, -10626905),
                array(29796507, 37186, 19818052, 10115756, -11829032, 3352736, 18551198, 3272828, -5190932, -4162409),
            ),
            array(
                array(12501286, 4044383, -8612957, -13392385, -32430052, 5136599, -19230378, -3529697, 330070, -3659409),
                array(6384877, 2899513, 17807477, 7663917, -2358888, 12363165, 25366522, -8573892, -271295, 12071499),
                array(-8365515, -4042521, 25133448, -4517355, -6211027, 2265927, -32769618, 1936675, -5159697, 3829363),
            ),
            array(
                array(28425966, -5835433, -577090, -4697198, -14217555, 6870930, 7921550, -6567787, 26333140, 14267664),
                array(-11067219, 11871231, 27385719, -10559544, -4585914, -11189312, 10004786, -8709488, -21761224, 8930324),
                array(-21197785, -16396035, 25654216, -1725397, 12282012, 11008919, 1541940, 4757911, -26491501, -16408940),
            ),
            array(
                array(13537262, -7759490, -20604840, 10961927, -5922820, -13218065, -13156584, 6217254, -15943699, 13814990),
                array(-17422573, 15157790, 18705543, 29619, 24409717, -260476, 27361681, 9257833, -1956526, -1776914),
                array(-25045300, -10191966, 15366585, 15166509, -13105086, 8423556, -29171540, 12361135, -18685978, 4578290),
            ),
            array(
                array(24579768, 3711570, 1342322, -11180126, -27005135, 14124956, -22544529, 14074919, 21964432, 8235257),
                array(-6528613, -2411497, 9442966, -5925588, 12025640, -1487420, -2981514, -1669206, 13006806, 2355433),
                array(-16304899, -13605259, -6632427, -5142349, 16974359, -10911083, 27202044, 1719366, 1141648, -12796236),
            ),
            array(
                array(-12863944, -13219986, -8318266, -11018091, -6810145, -4843894, 13475066, -3133972, 32674895, 13715045),
                array(11423335, -5468059, 32344216, 8962751, 24989809, 9241752, -13265253, 16086212, -28740881, -15642093),
                array(-1409668, 12530728, -6368726, 10847387, 19531186, -14132160, -11709148, 7791794, -27245943, 4383347),
            ),
        ),
        array(
            array(
                array(-28970898, 5271447, -1266009, -9736989, -12455236, 16732599, -4862407, -4906449, 27193557, 6245191),
                array(-15193956, 5362278, -1783893, 2695834, 4960227, 12840725, 23061898, 3260492, 22510453, 8577507),
                array(-12632451, 11257346, -32692994, 13548177, -721004, 10879011, 31168030, 13952092, -29571492, -3635906),
            ),
            array(
                array(3877321, -9572739, 32416692, 5405324, -11004407, -13656635, 3759769, 11935320, 5611860, 8164018),
                array(-16275802, 14667797, 15906460, 12155291, -22111149, -9039718, 32003002, -8832289, 5773085, -8422109),
                array(-23788118, -8254300, 1950875, 8937633, 18686727, 16459170, -905725, 12376320, 31632953, 190926),
            ),
            array(
                array(-24593607, -16138885, -8423991, 13378746, 14162407, 6901328, -8288749, 4508564, -25341555, -3627528),
                array(8884438, -5884009, 6023974, 10104341, -6881569, -4941533, 18722941, -14786005, -1672488, 827625),
                array(-32720583, -16289296, -32503547, 7101210, 13354605, 2659080, -1800575, -14108036, -24878478, 1541286),
            ),
            array(
                array(2901347, -1117687, 3880376, -10059388, -17620940, -3612781, -21802117, -3567481, 20456845, -1885033),
                array(27019610, 12299467, -13658288, -1603234, -12861660, -4861471, -19540150, -5016058, 29439641, 15138866),
                array(21536104, -6626420, -32447818, -10690208, -22408077, 5175814, -5420040, -16361163, 7779328, 109896),
            ),
            array(
                array(30279744, 14648750, -8044871, 6425558, 13639621, -743509, 28698390, 12180118, 23177719, -554075),
                array(26572847, 3405927, -31701700, 12890905, -19265668, 5335866, -6493768, 2378492, 4439158, -13279347),
                array(-22716706, 3489070, -9225266, -332753, 18875722, -1140095, 14819434, -12731527, -17717757, -5461437),
            ),
            array(
                array(-5056483, 16566551, 15953661, 3767752, -10436499, 15627060, -820954, 2177225, 8550082, -15114165),
                array(-18473302, 16596775, -381660, 15663611, 22860960, 15585581, -27844109, -3582739, -23260460, -8428588),
                array(-32480551, 15707275, -8205912, -5652081, 29464558, 2713815, -22725137, 15860482, -21902570, 1494193),
            ),
            array(
                array(-19562091, -14087393, -25583872, -9299552, 13127842, 759709, 21923482, 16529112, 8742704, 12967017),
                array(-28464899, 1553205, 32536856, -10473729, -24691605, -406174, -8914625, -2933896, -29903758, 15553883),
                array(21877909, 3230008, 9881174, 10539357, -4797115, 2841332, 11543572, 14513274, 19375923, -12647961),
            ),
            array(
                array(8832269, -14495485, 13253511, 5137575, 5037871, 4078777, 24880818, -6222716, 2862653, 9455043),
                array(29306751, 5123106, 20245049, -14149889, 9592566, 8447059, -2077124, -2990080, 15511449, 4789663),
                array(-20679756, 7004547, 8824831, -9434977, -4045704, -3750736, -5754762, 108893, 23513200, 16652362),
            ),
        ),
        array(
            array(
                array(-33256173, 4144782, -4476029, -6579123, 10770039, -7155542, -6650416, -12936300, -18319198, 10212860),
                array(2756081, 8598110, 7383731, -6859892, 22312759, -1105012, 21179801, 2600940, -9988298, -12506466),
                array(-24645692, 13317462, -30449259, -15653928, 21365574, -10869657, 11344424, 864440, -2499677, -16710063),
            ),
            array(
                array(-26432803, 6148329, -17184412, -14474154, 18782929, -275997, -22561534, 211300, 2719757, 4940997),
                array(-1323882, 3911313, -6948744, 14759765, -30027150, 7851207, 21690126, 8518463, 26699843, 5276295),
                array(-13149873, -6429067, 9396249, 365013, 24703301, -10488939, 1321586, 149635, -15452774, 7159369),
            ),
            array(
                array(9987780, -3404759, 17507962, 9505530, 9731535, -2165514, 22356009, 8312176, 22477218, -8403385),
                array(18155857, -16504990, 19744716, 9006923, 15154154, -10538976, 24256460, -4864995, -22548173, 9334109),
                array(2986088, -4911893, 10776628, -3473844, 10620590, -7083203, -21413845, 14253545, -22587149, 536906),
            ),
            array(
                array(4377756, 8115836, 24567078, 15495314, 11625074, 13064599, 7390551, 10589625, 10838060, -15420424),
                array(-19342404, 867880, 9277171, -3218459, -14431572, -1986443, 19295826, -15796950, 6378260, 699185),
                array(7895026, 4057113, -7081772, -13077756, -17886831, -323126, -716039, 15693155, -5045064, -13373962),
            ),
            array(
                array(-7737563, -5869402, -14566319, -7406919, 11385654, 13201616, 31730678, -10962840, -3918636, -9669325),
                array(10188286, -15770834, -7336361, 13427543, 22223443, 14896287, 30743455, 7116568, -21786507, 5427593),
                array(696102, 13206899, 27047647, -10632082, 15285305, -9853179, 10798490, -4578720, 19236243, 12477404),
            ),
            array(
                array(-11229439, 11243796, -17054270, -8040865, -788228, -8167967, -3897669, 11180504, -23169516, 7733644),
                array(17800790, -14036179, -27000429, -11766671, 23887827, 3149671, 23466177, -10538171, 10322027, 15313801),
                array(26246234, 11968874, 32263343, -5468728, 6830755, -13323031, -15794704, -101982, -24449242, 10890804),
            ),
            array(
                array(-31365647, 10271363, -12660625, -6267268, 16690207, -13062544, -14982212, 16484931, 25180797, -5334884),
                array(-586574, 10376444, -32586414, -11286356, 19801893, 10997610, 2276632, 9482883, 316878, 13820577),
                array(-9882808, -4510367, -2115506, 16457136, -11100081, 11674996, 30756178, -7515054, 30696930, -3712849),
            ),
            array(
                array(32988917, -9603412, 12499366, 7910787, -10617257, -11931514, -7342816, -9985397, -32349517, 7392473),
                array(-8855661, 15927861, 9866406, -3649411, -2396914, -16655781, -30409476, -9134995, 25112947, -2926644),
                array(-2504044, -436966, 25621774, -5678772, 15085042, -5479877, -24884878, -13526194, 5537438, -13914319),
            ),
        ),
        array(
            array(
                array(-11225584, 2320285, -9584280, 10149187, -33444663, 5808648, -14876251, -1729667, 31234590, 6090599),
                array(-9633316, 116426, 26083934, 2897444, -6364437, -2688086, 609721, 15878753, -6970405, -9034768),
                array(-27757857, 247744, -15194774, -9002551, 23288161, -10011936, -23869595, 6503646, 20650474, 1804084),
            ),
            array(
                array(-27589786, 15456424, 8972517, 8469608, 15640622, 4439847, 3121995, -10329713, 27842616, -202328),
                array(-15306973, 2839644, 22530074, 10026331, 4602058, 5048462, 28248656, 5031932, -11375082, 12714369),
                array(20807691, -7270825, 29286141, 11421711, -27876523, -13868230, -21227475, 1035546, -19733229, 12796920),
            ),
            array(
                array(12076899, -14301286, -8785001, -11848922, -25012791, 16400684, -17591495, -12899438, 3480665, -15182815),
                array(-32361549, 5457597, 28548107, 7833186, 7303070, -11953545, -24363064, -15921875, -33374054, 2771025),
                array(-21389266, 421932, 26597266, 6860826, 22486084, -6737172, -17137485, -4210226, -24552282, 15673397),
            ),
            array(
                array(-20184622, 2338216, 19788685, -9620956, -4001265, -8740893, -20271184, 4733254, 3727144, -12934448),
                array(6120119, 814863, -11794402, -622716, 6812205, -15747771, 2019594, 7975683, 31123697, -10958981),
                array(30069250, -11435332, 30434654, 2958439, 18399564, -976289, 12296869, 9204260, -16432438, 9648165),
            ),
            array(
                array(32705432, -1550977, 30705658, 7451065, -11805606, 9631813, 3305266, 5248604, -26008332, -11377501),
                array(17219865, 2375039, -31570947, -5575615, -19459679, 9219903, 294711, 15298639, 2662509, -16297073),
                array(-1172927, -7558695, -4366770, -4287744, -21346413, -8434326, 32087529, -1222777, 32247248, -14389861),
            ),
            array(
                array(14312628, 1221556, 17395390, -8700143, -4945741, -8684635, -28197744, -9637817, -16027623, -13378845),
                array(-1428825, -9678990, -9235681, 6549687, -7383069, -468664, 23046502, 9803137, 17597934, 2346211),
                array(18510800, 15337574, 26171504, 981392, -22241552, 7827556, -23491134, -11323352, 3059833, -11782870),
            ),
            array(
                array(10141598, 6082907, 17829293, -1947643, 9830092, 13613136, -25556636, -5544586, -33502212, 3592096),
                array(33114168, -15889352, -26525686, -13343397, 33076705, 8716171, 1151462, 1521897, -982665, -6837803),
                array(-32939165, -4255815, 23947181, -324178, -33072974, -12305637, -16637686, 3891704, 26353178, 693168),
            ),
            array(
                array(30374239, 1595580, -16884039, 13186931, 4600344, 406904, 9585294, -400668, 31375464, 14369965),
                array(-14370654, -7772529, 1510301, 6434173, -18784789, -6262728, 32732230, -13108839, 17901441, 16011505),
                array(18171223, -11934626, -12500402, 15197122, -11038147, -15230035, -19172240, -16046376, 8764035, 12309598),
            ),
        ),
        array(
            array(
                array(5975908, -5243188, -19459362, -9681747, -11541277, 14015782, -23665757, 1228319, 17544096, -10593782),
                array(5811932, -1715293, 3442887, -2269310, -18367348, -8359541, -18044043, -15410127, -5565381, 12348900),
                array(-31399660, 11407555, 25755363, 6891399, -3256938, 14872274, -24849353, 8141295, -10632534, -585479),
            ),
            array(
                array(-12675304, 694026, -5076145, 13300344, 14015258, -14451394, -9698672, -11329050, 30944593, 1130208),
                array(8247766, -6710942, -26562381, -7709309, -14401939, -14648910, 4652152, 2488540, 23550156, -271232),
                array(17294316, -3788438, 7026748, 15626851, 22990044, 113481, 2267737, -5908146, -408818, -137719),
            ),
            array(
                array(16091085, -16253926, 18599252, 7340678, 2137637, -1221657, -3364161, 14550936, 3260525, -7166271),
                array(-4910104, -13332887, 18550887, 10864893, -16459325, -7291596, -23028869, -13204905, -12748722, 2701326),
                array(-8574695, 16099415, 4629974, -16340524, -20786213, -6005432, -10018363, 9276971, 11329923, 1862132),
            ),
            array(
                array(14763076, -15903608, -30918270, 3689867, 3511892, 10313526, -21951088, 12219231, -9037963, -940300),
                array(8894987, -3446094, 6150753, 3013931, 301220, 15693451, -31981216, -2909717, -15438168, 11595570),
                array(15214962, 3537601, -26238722, -14058872, 4418657, -15230761, 13947276, 10730794, -13489462, -4363670),
            ),
            array(
                array(-2538306, 7682793, 32759013, 263109, -29984731, -7955452, -22332124, -10188635, 977108, 699994),
                array(-12466472, 4195084, -9211532, 550904, -15565337, 12917920, 19118110, -439841, -30534533, -14337913),
                array(31788461, -14507657, 4799989, 7372237, 8808585, -14747943, 9408237, -10051775, 12493932, -5409317),
            ),
            array(
                array(-25680606, 5260744, -19235809, -6284470, -3695942, 16566087, 27218280, 2607121, 29375955, 6024730),
                array(842132, -2794693, -4763381, -8722815, 26332018, -12405641, 11831880, 6985184, -9940361, 2854096),
                array(-4847262, -7969331, 2516242, -5847713, 9695691, -7221186, 16512645, 960770, 12121869, 16648078),
            ),
            array(
                array(-15218652, 14667096, -13336229, 2013717, 30598287, -464137, -31504922, -7882064, 20237806, 2838411),
                array(-19288047, 4453152, 15298546, -16178388, 22115043, -15972604, 12544294, -13470457, 1068881, -12499905),
                array(-9558883, -16518835, 33238498, 13506958, 30505848, -1114596, -8486907, -2630053, 12521378, 4845654),
            ),
            array(
                array(-28198521, 10744108, -2958380, 10199664, 7759311, -13088600, 3409348, -873400, -6482306, -12885870),
                array(-23561822, 6230156, -20382013, 10655314, -24040585, -11621172, 10477734, -1240216, -3113227, 13974498),
                array(12966261, 15550616, -32038948, -1615346, 21025980, -629444, 5642325, 7188737, 18895762, 12629579),
            ),
        ),
        array(
            array(
                array(14741879, -14946887, 22177208, -11721237, 1279741, 8058600, 11758140, 789443, 32195181, 3895677),
                array(10758205, 15755439, -4509950, 9243698, -4879422, 6879879, -2204575, -3566119, -8982069, 4429647),
                array(-2453894, 15725973, -20436342, -10410672, -5803908, -11040220, -7135870, -11642895, 18047436, -15281743),
            ),
            array(
                array(-25173001, -11307165, 29759956, 11776784, -22262383, -15820455, 10993114, -12850837, -17620701, -9408468),
                array(21987233, 700364, -24505048, 14972008, -7774265, -5718395, 32155026, 2581431, -29958985, 8773375),
                array(-25568350, 454463, -13211935, 16126715, 25240068, 8594567, 20656846, 12017935, -7874389, -13920155),
            ),
            array(
                array(6028182, 6263078, -31011806, -11301710, -818919, 2461772, -31841174, -5468042, -1721788, -2776725),
                array(-12278994, 16624277, 987579, -5922598, 32908203, 1248608, 7719845, -4166698, 28408820, 6816612),
                array(-10358094, -8237829, 19549651, -12169222, 22082623, 16147817, 20613181, 13982702, -10339570, 5067943),
            ),
            array(
                array(-30505967, -3821767, 12074681, 13582412, -19877972, 2443951, -19719286, 12746132, 5331210, -10105944),
                array(30528811, 3601899, -1957090, 4619785, -27361822, -15436388, 24180793, -12570394, 27679908, -1648928),
                array(9402404, -13957065, 32834043, 10838634, -26580150, -13237195, 26653274, -8685565, 22611444, -12715406),
            ),
            array(
                array(22190590, 1118029, 22736441, 15130463, -30460692, -5991321, 19189625, -4648942, 4854859, 6622139),
                array(-8310738, -2953450, -8262579, -3388049, -10401731, -271929, 13424426, -3567227, 26404409, 13001963),
                array(-31241838, -15415700, -2994250, 8939346, 11562230, -12840670, -26064365, -11621720, -15405155, 11020693),
            ),
            array(
                array(1866042, -7949489, -7898649, -10301010, 12483315, 13477547, 3175636, -12424163, 28761762, 1406734),
                array(-448555, -1777666, 13018551, 3194501, -9580420, -11161737, 24760585, -4347088, 25577411, -13378680),
                array(-24290378, 4759345, -690653, -1852816, 2066747, 10693769, -29595790, 9884936, -9368926, 4745410),
            ),
            array(
                array(-9141284, 6049714, -19531061, -4341411, -31260798, 9944276, -15462008, -11311852, 10931924, -11931931),
                array(-16561513, 14112680, -8012645, 4817318, -8040464, -11414606, -22853429, 10856641, -20470770, 13434654),
                array(22759489, -10073434, -16766264, -1871422, 13637442, -10168091, 1765144, -12654326, 28445307, -5364710),
            ),
            array(
                array(29875063, 12493613, 2795536, -3786330, 1710620, 15181182, -10195717, -8788675, 9074234, 1167180),
                array(-26205683, 11014233, -9842651, -2635485, -26908120, 7532294, -18716888, -9535498, 3843903, 9367684),
                array(-10969595, -6403711, 9591134, 9582310, 11349256, 108879, 16235123, 8601684, -139197, 4242895),
            ),
        ),
        array(
            array(
                array(22092954, -13191123, -2042793, -11968512, 32186753, -11517388, -6574341, 2470660, -27417366, 16625501),
                array(-11057722, 3042016, 13770083, -9257922, 584236, -544855, -7770857, 2602725, -27351616, 14247413),
                array(6314175, -10264892, -32772502, 15957557, -10157730, 168750, -8618807, 14290061, 27108877, -1180880),
            ),
            array(
                array(-8586597, -7170966, 13241782, 10960156, -32991015, -13794596, 33547976, -11058889, -27148451, 981874),
                array(22833440, 9293594, -32649448, -13618667, -9136966, 14756819, -22928859, -13970780, -10479804, -16197962),
                array(-7768587, 3326786, -28111797, 10783824, 19178761, 14905060, 22680049, 13906969, -15933690, 3797899),
            ),
            array(
                array(21721356, -4212746, -12206123, 9310182, -3882239, -13653110, 23740224, -2709232, 20491983, -8042152),
                array(9209270, -15135055, -13256557, -6167798, -731016, 15289673, 25947805, 15286587, 30997318, -6703063),
                array(7392032, 16618386, 23946583, -8039892, -13265164, -1533858, -14197445, -2321576, 17649998, -250080),
            ),
            array(
                array(-9301088, -14193827, 30609526, -3049543, -25175069, -1283752, -15241566, -9525724, -2233253, 7662146),
                array(-17558673, 1763594, -33114336, 15908610, -30040870, -12174295, 7335080, -8472199, -3174674, 3440183),
                array(-19889700, -5977008, -24111293, -9688870, 10799743, -16571957, 40450, -4431835, 4862400, 1133),
            ),
            array(
                array(-32856209, -7873957, -5422389, 14860950, -16319031, 7956142, 7258061, 311861, -30594991, -7379421),
                array(-3773428, -1565936, 28985340, 7499440, 24445838, 9325937, 29727763, 16527196, 18278453, 15405622),
                array(-4381906, 8508652, -19898366, -3674424, -5984453, 15149970, -13313598, 843523, -21875062, 13626197),
            ),
            array(
                array(2281448, -13487055, -10915418, -2609910, 1879358, 16164207, -10783882, 3953792, 13340839, 15928663),
                array(31727126, -7179855, -18437503, -8283652, 2875793, -16390330, -25269894, -7014826, -23452306, 5964753),
                array(4100420, -5959452, -17179337, 6017714, -18705837, 12227141, -26684835, 11344144, 2538215, -7570755),
            ),
            array(
                array(-9433605, 6123113, 11159803, -2156608, 30016280, 14966241, -20474983, 1485421, -629256, -15958862),
                array(-26804558, 4260919, 11851389, 9658551, -32017107, 16367492, -20205425, -13191288, 11659922, -11115118),
                array(26180396, 10015009, -30844224, -8581293, 5418197, 9480663, 2231568, -10170080, 33100372, -1306171),
            ),
            array(
                array(15121113, -5201871, -10389905, 15427821, -27509937, -15992507, 21670947, 4486675, -5931810, -14466380),
                array(16166486, -9483733, -11104130, 6023908, -31926798, -1364923, 2340060, -16254968, -10735770, -10039824),
                array(28042865, -3557089, -12126526, 12259706, -3717498, -6945899, 6766453, -8689599, 18036436, 5803270),
            ),
        ),
        array(
            array(
                array(-817581, 6763912, 11803561, 1585585, 10958447, -2671165, 23855391, 4598332, -6159431, -14117438),
                array(-31031306, -14256194, 17332029, -2383520, 31312682, -5967183, 696309, 50292, -20095739, 11763584),
                array(-594563, -2514283, -32234153, 12643980, 12650761, 14811489, 665117, -12613632, -19773211, -10713562),
            ),
            array(
                array(30464590, -11262872, -4127476, -12734478, 19835327, -7105613, -24396175, 2075773, -17020157, 992471),
                array(18357185, -6994433, 7766382, 16342475, -29324918, 411174, 14578841, 8080033, -11574335, -10601610),
                array(19598397, 10334610, 12555054, 2555664, 18821899, -10339780, 21873263, 16014234, 26224780, 16452269),
            ),
            array(
                array(-30223925, 5145196, 5944548, 16385966, 3976735, 2009897, -11377804, -7618186, -20533829, 3698650),
                array(14187449, 3448569, -10636236, -10810935, -22663880, -3433596, 7268410, -10890444, 27394301, 12015369),
                array(19695761, 16087646, 28032085, 12999827, 6817792, 11427614, 20244189, -1312777, -13259127, -3402461),
            ),
            array(
                array(30860103, 12735208, -1888245, -4699734, -16974906, 2256940, -8166013, 12298312, -8550524, -10393462),
                array(-5719826, -11245325, -1910649, 15569035, 26642876, -7587760, -5789354, -15118654, -4976164, 12651793),
                array(-2848395, 9953421, 11531313, -5282879, 26895123, -12697089, -13118820, -16517902, 9768698, -2533218),
            ),
            array(
                array(-24719459, 1894651, -287698, -4704085, 15348719, -8156530, 32767513, 12765450, 4940095, 10678226),
                array(18860224, 15980149, -18987240, -1562570, -26233012, -11071856, -7843882, 13944024, -24372348, 16582019),
                array(-15504260, 4970268, -29893044, 4175593, -20993212, -2199756, -11704054, 15444560, -11003761, 7989037),
            ),
            array(
                array(31490452, 5568061, -2412803, 2182383, -32336847, 4531686, -32078269, 6200206, -19686113, -14800171),
                array(-17308668, -15879940, -31522777, -2831, -32887382, 16375549, 8680158, -16371713, 28550068, -6857132),
                array(-28126887, -5688091, 16837845, -1820458, -6850681, 12700016, -30039981, 4364038, 1155602, 5988841),
            ),
            array(
                array(21890435, -13272907, -12624011, 12154349, -7831873, 15300496, 23148983, -4470481, 24618407, 8283181),
                array(-33136107, -10512751, 9975416, 6841041, -31559793, 16356536, 3070187, -7025928, 1466169, 10740210),
                array(-1509399, -15488185, -13503385, -10655916, 32799044, 909394, -13938903, -5779719, -32164649, -15327040),
            ),
            array(
                array(3960823, -14267803, -28026090, -15918051, -19404858, 13146868, 15567327, 951507, -3260321, -573935),
                array(24740841, 5052253, -30094131, 8961361, 25877428, 6165135, -24368180, 14397372, -7380369, -6144105),
                array(-28888365, 3510803, -28103278, -1158478, -11238128, -10631454, -15441463, -14453128, -1625486, -6494814),
            ),
        ),
        array(
            array(
                array(793299, -9230478, 8836302, -6235707, -27360908, -2369593, 33152843, -4885251, -9906200, -621852),
                array(5666233, 525582, 20782575, -8038419, -24538499, 14657740, 16099374, 1468826, -6171428, -15186581),
                array(-4859255, -3779343, -2917758, -6748019, 7778750, 11688288, -30404353, -9871238, -1558923, -9863646),
            ),
            array(
                array(10896332, -7719704, 824275, 472601, -19460308, 3009587, 25248958, 14783338, -30581476, -15757844),
                array(10566929, 12612572, -31944212, 11118703, -12633376, 12362879, 21752402, 8822496, 24003793, 14264025),
                array(27713862, -7355973, -11008240, 9227530, 27050101, 2504721, 23886875, -13117525, 13958495, -5732453),
            ),
            array(
                array(-23481610, 4867226, -27247128, 3900521, 29838369, -8212291, -31889399, -10041781, 7340521, -15410068),
                array(4646514, -8011124, -22766023, -11532654, 23184553, 8566613, 31366726, -1381061, -15066784, -10375192),
                array(-17270517, 12723032, -16993061, 14878794, 21619651, -6197576, 27584817, 3093888, -8843694, 3849921),
            ),
            array(
                array(-9064912, 2103172, 25561640, -15125738, -5239824, 9582958, 32477045, -9017955, 5002294, -15550259),
                array(-12057553, -11177906, 21115585, -13365155, 8808712, -12030708, 16489530, 13378448, -25845716, 12741426),
                array(-5946367, 10645103, -30911586, 15390284, -3286982, -7118677, 24306472, 15852464, 28834118, -7646072),
            ),
            array(
                array(-17335748, -9107057, -24531279, 9434953, -8472084, -583362, -13090771, 455841, 20461858, 5491305),
                array(13669248, -16095482, -12481974, -10203039, -14569770, -11893198, -24995986, 11293807, -28588204, -9421832),
                array(28497928, 6272777, -33022994, 14470570, 8906179, -1225630, 18504674, -14165166, 29867745, -8795943),
            ),
            array(
                array(-16207023, 13517196, -27799630, -13697798, 24009064, -6373891, -6367600, -13175392, 22853429, -4012011),
                array(24191378, 16712145, -13931797, 15217831, 14542237, 1646131, 18603514, -11037887, 12876623, -2112447),
                array(17902668, 4518229, -411702, -2829247, 26878217, 5258055, -12860753, 608397, 16031844, 3723494),
            ),
            array(
                array(-28632773, 12763728, -20446446, 7577504, 33001348, -13017745, 17558842, -7872890, 23896954, -4314245),
                array(-20005381, -12011952, 31520464, 605201, 2543521, 5991821, -2945064, 7229064, -9919646, -8826859),
                array(28816045, 298879, -28165016, -15920938, 19000928, -1665890, -12680833, -2949325, -18051778, -2082915),
            ),
            array(
                array(16000882, -344896, 3493092, -11447198, -29504595, -13159789, 12577740, 16041268, -19715240, 7847707),
                array(10151868, 10572098, 27312476, 7922682, 14825339, 4723128, -32855931, -6519018, -10020567, 3852848),
                array(-11430470, 15697596, -21121557, -4420647, 5386314, 15063598, 16514493, -15932110, 29330899, -15076224),
            ),
        ),
        array(
            array(
                array(-25499735, -4378794, -15222908, -6901211, 16615731, 2051784, 3303702, 15490, -27548796, 12314391),
                array(15683520, -6003043, 18109120, -9980648, 15337968, -5997823, -16717435, 15921866, 16103996, -3731215),
                array(-23169824, -10781249, 13588192, -1628807, -3798557, -1074929, -19273607, 5402699, -29815713, -9841101),
            ),
            array(
                array(23190676, 2384583, -32714340, 3462154, -29903655, -1529132, -11266856, 8911517, -25205859, 2739713),
                array(21374101, -3554250, -33524649, 9874411, 15377179, 11831242, -33529904, 6134907, 4931255, 11987849),
                array(-7732, -2978858, -16223486, 7277597, 105524, -322051, -31480539, 13861388, -30076310, 10117930),
            ),
            array(
                array(-29501170, -10744872, -26163768, 13051539, -25625564, 5089643, -6325503, 6704079, 12890019, 15728940),
                array(-21972360, -11771379, -951059, -4418840, 14704840, 2695116, 903376, -10428139, 12885167, 8311031),
                array(-17516482, 5352194, 10384213, -13811658, 7506451, 13453191, 26423267, 4384730, 1888765, -5435404),
            ),
            array(
                array(-25817338, -3107312, -13494599, -3182506, 30896459, -13921729, -32251644, -12707869, -19464434, -3340243),
                array(-23607977, -2665774, -526091, 4651136, 5765089, 4618330, 6092245, 14845197, 17151279, -9854116),
                array(-24830458, -12733720, -15165978, 10367250, -29530908, -265356, 22825805, -7087279, -16866484, 16176525),
            ),
            array(
                array(-23583256, 6564961, 20063689, 3798228, -4740178, 7359225, 2006182, -10363426, -28746253, -10197509),
                array(-10626600, -4486402, -13320562, -5125317, 3432136, -6393229, 23632037, -1940610, 32808310, 1099883),
                array(15030977, 5768825, -27451236, -2887299, -6427378, -15361371, -15277896, -6809350, 2051441, -15225865),
            ),
            array(
                array(-3362323, -7239372, 7517890, 9824992, 23555850, 295369, 5148398, -14154188, -22686354, 16633660),
                array(4577086, -16752288, 13249841, -15304328, 19958763, -14537274, 18559670, -10759549, 8402478, -9864273),
                array(-28406330, -1051581, -26790155, -907698, -17212414, -11030789, 9453451, -14980072, 17983010, 9967138),
            ),
            array(
                array(-25762494, 6524722, 26585488, 9969270, 24709298, 1220360, -1677990, 7806337, 17507396, 3651560),
                array(-10420457, -4118111, 14584639, 15971087, -15768321, 8861010, 26556809, -5574557, -18553322, -11357135),
                array(2839101, 14284142, 4029895, 3472686, 14402957, 12689363, -26642121, 8459447, -5605463, -7621941),
            ),
            array(
                array(-4839289, -3535444, 9744961, 2871048, 25113978, 3187018, -25110813, -849066, 17258084, -7977739),
                array(18164541, -10595176, -17154882, -1542417, 19237078, -9745295, 23357533, -15217008, 26908270, 12150756),
                array(-30264870, -7647865, 5112249, -7036672, -1499807, -6974257, 43168, -5537701, -32302074, 16215819),
            ),
        ),
        array(
            array(
                array(-6898905, 9824394, -12304779, -4401089, -31397141, -6276835, 32574489, 12532905, -7503072, -8675347),
                array(-27343522, -16515468, -27151524, -10722951, 946346, 16291093, 254968, 7168080, 21676107, -1943028),
                array(21260961, -8424752, -16831886, -11920822, -23677961, 3968121, -3651949, -6215466, -3556191, -7913075),
            ),
            array(
                array(16544754, 13250366, -16804428, 15546242, -4583003, 12757258, -2462308, -8680336, -18907032, -9662799),
                array(-2415239, -15577728, 18312303, 4964443, -15272530, -12653564, 26820651, 16690659, 25459437, -4564609),
                array(-25144690, 11425020, 28423002, -11020557, -6144921, -15826224, 9142795, -2391602, -6432418, -1644817),
            ),
            array(
                array(-23104652, 6253476, 16964147, -3768872, -25113972, -12296437, -27457225, -16344658, 6335692, 7249989),
                array(-30333227, 13979675, 7503222, -12368314, -11956721, -4621693, -30272269, 2682242, 25993170, -12478523),
                array(4364628, 5930691, 32304656, -10044554, -8054781, 15091131, 22857016, -10598955, 31820368, 15075278),
            ),
            array(
                array(31879134, -8918693, 17258761, 90626, -8041836, -4917709, 24162788, -9650886, -17970238, 12833045),
                array(19073683, 14851414, -24403169, -11860168, 7625278, 11091125, -19619190, 2074449, -9413939, 14905377),
                array(24483667, -11935567, -2518866, -11547418, -1553130, 15355506, -25282080, 9253129, 27628530, -7555480),
            ),
            array(
                array(17597607, 8340603, 19355617, 552187, 26198470, -3176583, 4593324, -9157582, -14110875, 15297016),
                array(510886, 14337390, -31785257, 16638632, 6328095, 2713355, -20217417, -11864220, 8683221, 2921426),
                array(18606791, 11874196, 27155355, -5281482, -24031742, 6265446, -25178240, -1278924, 4674690, 13890525),
            ),
            array(
                array(13609624, 13069022, -27372361, -13055908, 24360586, 9592974, 14977157, 9835105, 4389687, 288396),
                array(9922506, -519394, 13613107, 5883594, -18758345, -434263, -12304062, 8317628, 23388070, 16052080),
                array(12720016, 11937594, -31970060, -5028689, 26900120, 8561328, -20155687, -11632979, -14754271, -10812892),
            ),
            array(
                array(15961858, 14150409, 26716931, -665832, -22794328, 13603569, 11829573, 7467844, -28822128, 929275),
                array(11038231, -11582396, -27310482, -7316562, -10498527, -16307831, -23479533, -9371869, -21393143, 2465074),
                array(20017163, -4323226, 27915242, 1529148, 12396362, 15675764, 13817261, -9658066, 2463391, -4622140),
            ),
            array(
                array(-16358878, -12663911, -12065183, 4996454, -1256422, 1073572, 9583558, 12851107, 4003896, 12673717),
                array(-1731589, -15155870, -3262930, 16143082, 19294135, 13385325, 14741514, -9103726, 7903886, 2348101),
                array(24536016, -16515207, 12715592, -3862155, 1511293, 10047386, -3842346, -7129159, -28377538, 10048127),
            ),
        ),
        array(
            array(
                array(-12622226, -6204820, 30718825, 2591312, -10617028, 12192840, 18873298, -7297090, -32297756, 15221632),
                array(-26478122, -11103864, 11546244, -1852483, 9180880, 7656409, -21343950, 2095755, 29769758, 6593415),
                array(-31994208, -2907461, 4176912, 3264766, 12538965, -868111, 26312345, -6118678, 30958054, 8292160),
            ),
            array(
                array(31429822, -13959116, 29173532, 15632448, 12174511, -2760094, 32808831, 3977186, 26143136, -3148876),
                array(22648901, 1402143, -22799984, 13746059, 7936347, 365344, -8668633, -1674433, -3758243, -2304625),
                array(-15491917, 8012313, -2514730, -12702462, -23965846, -10254029, -1612713, -1535569, -16664475, 8194478),
            ),
            array(
                array(27338066, -7507420, -7414224, 10140405, -19026427, -6589889, 27277191, 8855376, 28572286, 3005164),
                array(26287124, 4821776, 25476601, -4145903, -3764513, -15788984, -18008582, 1182479, -26094821, -13079595),
                array(-7171154, 3178080, 23970071, 6201893, -17195577, -4489192, -21876275, -13982627, 32208683, -1198248),
            ),
            array(
                array(-16657702, 2817643, -10286362, 14811298, 6024667, 13349505, -27315504, -10497842, -27672585, -11539858),
                array(15941029, -9405932, -21367050, 8062055, 31876073, -238629, -15278393, -1444429, 15397331, -4130193),
                array(8934485, -13485467, -23286397, -13423241, -32446090, 14047986, 31170398, -1441021, -27505566, 15087184),
            ),
            array(
                array(-18357243, -2156491, 24524913, -16677868, 15520427, -6360776, -15502406, 11461896, 16788528, -5868942),
                array(-1947386, 16013773, 21750665, 3714552, -17401782, -16055433, -3770287, -10323320, 31322514, -11615635),
                array(21426655, -5650218, -13648287, -5347537, -28812189, -4920970, -18275391, -14621414, 13040862, -12112948),
            ),
            array(
                array(11293895, 12478086, -27136401, 15083750, -29307421, 14748872, 14555558, -13417103, 1613711, 4896935),
                array(-25894883, 15323294, -8489791, -8057900, 25967126, -13425460, 2825960, -4897045, -23971776, -11267415),
                array(-15924766, -5229880, -17443532, 6410664, 3622847, 10243618, 20615400, 12405433, -23753030, -8436416),
            ),
            array(
                array(-7091295, 12556208, -20191352, 9025187, -17072479, 4333801, 4378436, 2432030, 23097949, -566018),
                array(4565804, -16025654, 20084412, -7842817, 1724999, 189254, 24767264, 10103221, -18512313, 2424778),
                array(366633, -11976806, 8173090, -6890119, 30788634, 5745705, -7168678, 1344109, -3642553, 12412659),
            ),
            array(
                array(-24001791, 7690286, 14929416, -168257, -32210835, -13412986, 24162697, -15326504, -3141501, 11179385),
                array(18289522, -14724954, 8056945, 16430056, -21729724, 7842514, -6001441, -1486897, -18684645, -11443503),
                array(476239, 6601091, -6152790, -9723375, 17503545, -4863900, 27672959, 13403813, 11052904, 5219329),
            ),
        ),
        array(
            array(
                array(20678546, -8375738, -32671898, 8849123, -5009758, 14574752, 31186971, -3973730, 9014762, -8579056),
                array(-13644050, -10350239, -15962508, 5075808, -1514661, -11534600, -33102500, 9160280, 8473550, -3256838),
                array(24900749, 14435722, 17209120, -15292541, -22592275, 9878983, -7689309, -16335821, -24568481, 11788948),
            ),
            array(
                array(-3118155, -11395194, -13802089, 14797441, 9652448, -6845904, -20037437, 10410733, -24568470, -1458691),
                array(-15659161, 16736706, -22467150, 10215878, -9097177, 7563911, 11871841, -12505194, -18513325, 8464118),
                array(-23400612, 8348507, -14585951, -861714, -3950205, -6373419, 14325289, 8628612, 33313881, -8370517),
            ),
            array(
                array(-20186973, -4967935, 22367356, 5271547, -1097117, -4788838, -24805667, -10236854, -8940735, -5818269),
                array(-6948785, -1795212, -32625683, -16021179, 32635414, -7374245, 15989197, -12838188, 28358192, -4253904),
                array(-23561781, -2799059, -32351682, -1661963, -9147719, 10429267, -16637684, 4072016, -5351664, 5596589),
            ),
            array(
                array(-28236598, -3390048, 12312896, 6213178, 3117142, 16078565, 29266239, 2557221, 1768301, 15373193),
                array(-7243358, -3246960, -4593467, -7553353, -127927, -912245, -1090902, -4504991, -24660491, 3442910),
                array(-30210571, 5124043, 14181784, 8197961, 18964734, -11939093, 22597931, 7176455, -18585478, 13365930),
            ),
            array(
                array(-7877390, -1499958, 8324673, 4690079, 6261860, 890446, 24538107, -8570186, -9689599, -3031667),
                array(25008904, -10771599, -4305031, -9638010, 16265036, 15721635, 683793, -11823784, 15723479, -15163481),
                array(-9660625, 12374379, -27006999, -7026148, -7724114, -12314514, 11879682, 5400171, 519526, -1235876),
            ),
            array(
                array(22258397, -16332233, -7869817, 14613016, -22520255, -2950923, -20353881, 7315967, 16648397, 7605640),
                array(-8081308, -8464597, -8223311, 9719710, 19259459, -15348212, 23994942, -5281555, -9468848, 4763278),
                array(-21699244, 9220969, -15730624, 1084137, -25476107, -2852390, 31088447, -7764523, -11356529, 728112),
            ),
            array(
                array(26047220, -11751471, -6900323, -16521798, 24092068, 9158119, -4273545, -12555558, -29365436, -5498272),
                array(17510331, -322857, 5854289, 8403524, 17133918, -3112612, -28111007, 12327945, 10750447, 10014012),
                array(-10312768, 3936952, 9156313, -8897683, 16498692, -994647, -27481051, -666732, 3424691, 7540221),
            ),
            array(
                array(30322361, -6964110, 11361005, -4143317, 7433304, 4989748, -7071422, -16317219, -9244265, 15258046),
                array(13054562, -2779497, 19155474, 469045, -12482797, 4566042, 5631406, 2711395, 1062915, -5136345),
                array(-19240248, -11254599, -29509029, -7499965, -5835763, 13005411, -6066489, 12194497, 32960380, 1459310),
            ),
        ),
        array(
            array(
                array(19852034, 7027924, 23669353, 10020366, 8586503, -6657907, 394197, -6101885, 18638003, -11174937),
                array(31395534, 15098109, 26581030, 8030562, -16527914, -5007134, 9012486, -7584354, -6643087, -5442636),
                array(-9192165, -2347377, -1997099, 4529534, 25766844, 607986, -13222, 9677543, -32294889, -6456008),
            ),
            array(
                array(-2444496, -149937, 29348902, 8186665, 1873760, 12489863, -30934579, -7839692, -7852844, -8138429),
                array(-15236356, -15433509, 7766470, 746860, 26346930, -10221762, -27333451, 10754588, -9431476, 5203576),
                array(31834314, 14135496, -770007, 5159118, 20917671, -16768096, -7467973, -7337524, 31809243, 7347066),
            ),
            array(
                array(-9606723, -11874240, 20414459, 13033986, 13716524, -11691881, 19797970, -12211255, 15192876, -2087490),
                array(-12663563, -2181719, 1168162, -3804809, 26747877, -14138091, 10609330, 12694420, 33473243, -13382104),
                array(33184999, 11180355, 15832085, -11385430, -1633671, 225884, 15089336, -11023903, -6135662, 14480053),
            ),
            array(
                array(31308717, -5619998, 31030840, -1897099, 15674547, -6582883, 5496208, 13685227, 27595050, 8737275),
                array(-20318852, -15150239, 10933843, -16178022, 8335352, -7546022, -31008351, -12610604, 26498114, 66511),
                array(22644454, -8761729, -16671776, 4884562, -3105614, -13559366, 30540766, -4286747, -13327787, -7515095),
            ),
            array(
                array(-28017847, 9834845, 18617207, -2681312, -3401956, -13307506, 8205540, 13585437, -17127465, 15115439),
                array(23711543, -672915, 31206561, -8362711, 6164647, -9709987, -33535882, -1426096, 8236921, 16492939),
                array(-23910559, -13515526, -26299483, -4503841, 25005590, -7687270, 19574902, 10071562, 6708380, -6222424),
            ),
            array(
                array(2101391, -4930054, 19702731, 2367575, -15427167, 1047675, 5301017, 9328700, 29955601, -11678310),
                array(3096359, 9271816, -21620864, -15521844, -14847996, -7592937, -25892142, -12635595, -9917575, 6216608),
                array(-32615849, 338663, -25195611, 2510422, -29213566, -13820213, 24822830, -6146567, -26767480, 7525079),
            ),
            array(
                array(-23066649, -13985623, 16133487, -7896178, -3389565, 778788, -910336, -2782495, -19386633, 11994101),
                array(21691500, -13624626, -641331, -14367021, 3285881, -3483596, -25064666, 9718258, -7477437, 13381418),
                array(18445390, -4202236, 14979846, 11622458, -1727110, -3582980, 23111648, -6375247, 28535282, 15779576),
            ),
            array(
                array(30098053, 3089662, -9234387, 16662135, -21306940, 11308411, -14068454, 12021730, 9955285, -16303356),
                array(9734894, -14576830, -7473633, -9138735, 2060392, 11313496, -18426029, 9924399, 20194861, 13380996),
                array(-26378102, -7965207, -22167821, 15789297, -18055342, -6168792, -1984914, 15707771, 26342023, 10146099),
            ),
        ),
        array(
            array(
                array(-26016874, -219943, 21339191, -41388, 19745256, -2878700, -29637280, 2227040, 21612326, -545728),
                array(-13077387, 1184228, 23562814, -5970442, -20351244, -6348714, 25764461, 12243797, -20856566, 11649658),
                array(-10031494, 11262626, 27384172, 2271902, 26947504, -15997771, 39944, 6114064, 33514190, 2333242),
            ),
            array(
                array(-21433588, -12421821, 8119782, 7219913, -21830522, -9016134, -6679750, -12670638, 24350578, -13450001),
                array(-4116307, -11271533, -23886186, 4843615, -30088339, 690623, -31536088, -10406836, 8317860, 12352766),
                array(18200138, -14475911, -33087759, -2696619, -23702521, -9102511, -23552096, -2287550, 20712163, 6719373),
            ),
            array(
                array(26656208, 6075253, -7858556, 1886072, -28344043, 4262326, 11117530, -3763210, 26224235, -3297458),
                array(-17168938, -14854097, -3395676, -16369877, -19954045, 14050420, 21728352, 9493610, 18620611, -16428628),
                array(-13323321, 13325349, 11432106, 5964811, 18609221, 6062965, -5269471, -9725556, -30701573, -16479657),
            ),
            array(
                array(-23860538, -11233159, 26961357, 1640861, -32413112, -16737940, 12248509, -5240639, 13735342, 1934062),
                array(25089769, 6742589, 17081145, -13406266, 21909293, -16067981, -15136294, -3765346, -21277997, 5473616),
                array(31883677, -7961101, 1083432, -11572403, 22828471, 13290673, -7125085, 12469656, 29111212, -5451014),
            ),
            array(
                array(24244947, -15050407, -26262976, 2791540, -14997599, 16666678, 24367466, 6388839, -10295587, 452383),
                array(-25640782, -3417841, 5217916, 16224624, 19987036, -4082269, -24236251, -5915248, 15766062, 8407814),
                array(-20406999, 13990231, 15495425, 16395525, 5377168, 15166495, -8917023, -4388953, -8067909, 2276718),
            ),
            array(
                array(30157918, 12924066, -17712050, 9245753, 19895028, 3368142, -23827587, 5096219, 22740376, -7303417),
                array(2041139, -14256350, 7783687, 13876377, -25946985, -13352459, 24051124, 13742383, -15637599, 13295222),
                array(33338237, -8505733, 12532113, 7977527, 9106186, -1715251, -17720195, -4612972, -4451357, -14669444),
            ),
            array(
                array(-20045281, 5454097, -14346548, 6447146, 28862071, 1883651, -2469266, -4141880, 7770569, 9620597),
                array(23208068, 7979712, 33071466, 8149229, 1758231, -10834995, 30945528, -1694323, -33502340, -14767970),
                array(1439958, -16270480, -1079989, -793782, 4625402, 10647766, -5043801, 1220118, 30494170, -11440799),
            ),
            array(
                array(-5037580, -13028295, -2970559, -3061767, 15640974, -6701666, -26739026, 926050, -1684339, -13333647),
                array(13908495, -3549272, 30919928, -6273825, -21521863, 7989039, 9021034, 9078865, 3353509, 4033511),
                array(-29663431, -15113610, 32259991, -344482, 24295849, -12912123, 23161163, 8839127, 27485041, 7356032),
            ),
        ),
        array(
            array(
                array(9661027, 705443, 11980065, -5370154, -1628543, 14661173, -6346142, 2625015, 28431036, -16771834),
                array(-23839233, -8311415, -25945511, 7480958, -17681669, -8354183, -22545972, 14150565, 15970762, 4099461),
                array(29262576, 16756590, 26350592, -8793563, 8529671, -11208050, 13617293, -9937143, 11465739, 8317062),
            ),
            array(
                array(-25493081, -6962928, 32500200, -9419051, -23038724, -2302222, 14898637, 3848455, 20969334, -5157516),
                array(-20384450, -14347713, -18336405, 13884722, -33039454, 2842114, -21610826, -3649888, 11177095, 14989547),
                array(-24496721, -11716016, 16959896, 2278463, 12066309, 10137771, 13515641, 2581286, -28487508, 9930240),
            ),
            array(
                array(-17751622, -2097826, 16544300, -13009300, -15914807, -14949081, 18345767, -13403753, 16291481, -5314038),
                array(-33229194, 2553288, 32678213, 9875984, 8534129, 6889387, -9676774, 6957617, 4368891, 9788741),
                array(16660756, 7281060, -10830758, 12911820, 20108584, -8101676, -21722536, -8613148, 16250552, -11111103),
            ),
            array(
                array(-19765507, 2390526, -16551031, 14161980, 1905286, 6414907, 4689584, 10604807, -30190403, 4782747),
                array(-1354539, 14736941, -7367442, -13292886, 7710542, -14155590, -9981571, 4383045, 22546403, 437323),
                array(31665577, -12180464, -16186830, 1491339, -18368625, 3294682, 27343084, 2786261, -30633590, -14097016),
            ),
            array(
                array(-14467279, -683715, -33374107, 7448552, 19294360, 14334329, -19690631, 2355319, -19284671, -6114373),
                array(15121312, -15796162, 6377020, -6031361, -10798111, -12957845, 18952177, 15496498, -29380133, 11754228),
                array(-2637277, -13483075, 8488727, -14303896, 12728761, -1622493, 7141596, 11724556, 22761615, -10134141),
            ),
            array(
                array(16918416, 11729663, -18083579, 3022987, -31015732, -13339659, -28741185, -12227393, 32851222, 11717399),
                array(11166634, 7338049, -6722523, 4531520, -29468672, -7302055, 31474879, 3483633, -1193175, -4030831),
                array(-185635, 9921305, 31456609, -13536438, -12013818, 13348923, 33142652, 6546660, -19985279, -3948376),
            ),
            array(
                array(-32460596, 11266712, -11197107, -7899103, 31703694, 3855903, -8537131, -12833048, -30772034, -15486313),
                array(-18006477, 12709068, 3991746, -6479188, -21491523, -10550425, -31135347, -16049879, 10928917, 3011958),
                array(-6957757, -15594337, 31696059, 334240, 29576716, 14796075, -30831056, -12805180, 18008031, 10258577),
            ),
            array(
                array(-22448644, 15655569, 7018479, -4410003, -30314266, -1201591, -1853465, 1367120, 25127874, 6671743),
                array(29701166, -14373934, -10878120, 9279288, -17568, 13127210, 21382910, 11042292, 25838796, 4642684),
                array(-20430234, 14955537, -24126347, 8124619, -5369288, -5990470, 30468147, -13900640, 18423289, 4177476),
            ),
        )
    );

    /**
     * See: libsodium's crypto_core/curve25519/ref10/base2.h
     *
     * @var array<int, array<int, array<int, int>>> basically int[8][3]
     */
    protected static $base2 = array(
        array(
            array(25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605),
            array(-12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378),
            array(-8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546),
        ),
        array(
            array(15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024),
            array(16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574),
            array(30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357),
        ),
        array(
            array(10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380),
            array(4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306),
            array(19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942),
        ),
        array(
            array(5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766),
            array(-30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701),
            array(28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300),
        ),
        array(
            array(-22518993, -6692182, 14201702, -8745502, -23510406, 8844726, 18474211, -1361450, -13062696, 13821877),
            array(-6455177, -7839871, 3374702, -4740862, -27098617, -10571707, 31655028, -7212327, 18853322, -14220951),
            array(4566830, -12963868, -28974889, -12240689, -7602672, -2830569, -8514358, -10431137, 2207753, -3209784),
        ),
        array(
            array(-25154831, -4185821, 29681144, 7868801, -6854661, -9423865, -12437364, -663000, -31111463, -16132436),
            array(25576264, -2703214, 7349804, -11814844, 16472782, 9300885, 3844789, 15725684, 171356, 6466918),
            array(23103977, 13316479, 9739013, -16149481, 817875, -15038942, 8965339, -14088058, -30714912, 16193877),
        ),
        array(
            array(-33521811, 3180713, -2394130, 14003687, -16903474, -16270840, 17238398, 4729455, -18074513, 9256800),
            array(-25182317, -4174131, 32336398, 5036987, -21236817, 11360617, 22616405, 9761698, -19827198, 630305),
            array(-13720693, 2639453, -24237460, -7406481, 9494427, -5774029, -6554551, -15960994, -2449256, -14291300),
        ),
        array(
            array(-3151181, -5046075, 9282714, 6866145, -31907062, -863023, -18940575, 15033784, 25105118, -7894876),
            array(-24326370, 15950226, -31801215, -14592823, -11662737, -5090925, 1573892, -2625887, 2198790, -15804619),
            array(-3099351, 10324967, -2241613, 7453183, -5446979, -2735503, -13812022, -16236442, -32461234, -12290683),
        )
    );

    /**
     * 37095705934669439343138083508754565189542113879843219016388785533085940283555
     *
     * @var array<int, int>
     */
    protected static $d = array(
        -10913610,
        13857413,
        -15372611,
        6949391,
        114729,
        -8787816,
        -6275908,
        -3247719,
        -18696448,
        -12055116
    );

    /**
     * 2 * d = 16295367250680780974490674513165176452449235426866156013048779062215315747161
     *
     * @var array<int, int>
     */
    protected static $d2 = array(
        -21827239,
        -5839606,
        -30745221,
        13898782,
        229458,
        15978800,
        -12551817,
        -6495438,
        29715968,
        9444199
    );

    /**
     * sqrt(-1)
     *
     * @var array<int, int>
     */
    protected static $sqrtm1 = array(
        -32595792,
        -7943725,
        9377950,
        3500415,
        12389472,
        -272473,
        -25146209,
        -2005654,
        326686,
        11406482
    );
}
Core32/Curve25519/Ge/Cached.php000064400000003415152331075510011513 0ustar00<?php


if (class_exists('ParagonIE_Sodium_Core32_Curve25519_Ge_Cached', false)) {
    return;
}
/**
 * Class ParagonIE_Sodium_Core32_Curve25519_Ge_Cached
 */
class ParagonIE_Sodium_Core32_Curve25519_Ge_Cached
{
    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $YplusX;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $YminusX;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $Z;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $T2d;

    /**
     * ParagonIE_Sodium_Core32_Curve25519_Ge_Cached constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $YplusX
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $YminusX
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $Z
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $T2d
     */
    public function __construct(
        ParagonIE_Sodium_Core32_Curve25519_Fe $YplusX = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $YminusX = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $Z = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $T2d = null
    ) {
        if ($YplusX === null) {
            $YplusX = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        }
        $this->YplusX = $YplusX;
        if ($YminusX === null) {
            $YminusX = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        }
        $this->YminusX = $YminusX;
        if ($Z === null) {
            $Z = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        }
        $this->Z = $Z;
        if ($T2d === null) {
            $T2d = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        }
        $this->T2d = $T2d;
    }
}
Core32/Curve25519/Ge/P2.php000064400000002541152331075510010624 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_Curve25519_Ge_P2', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_Curve25519_Ge_P2
 */
class ParagonIE_Sodium_Core32_Curve25519_Ge_P2
{
    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $X;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $Y;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $Z;

    /**
     * ParagonIE_Sodium_Core32_Curve25519_Ge_P2 constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $x
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $y
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $z
     */
    public function __construct(
        ParagonIE_Sodium_Core32_Curve25519_Fe $x = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $y = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $z = null
    ) {
        if ($x === null) {
            $x = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        }
        $this->X = $x;
        if ($y === null) {
            $y = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        }
        $this->Y = $y;
        if ($z === null) {
            $z = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        }
        $this->Z = $z;
    }
}
Core32/Curve25519/Ge/P3.php000064400000003242152331075510010624 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_Curve25519_Ge_P3', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_Curve25519_Ge_P3
 */
class ParagonIE_Sodium_Core32_Curve25519_Ge_P3
{
    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $X;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $Y;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $Z;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $T;

    /**
     * ParagonIE_Sodium_Core32_Curve25519_Ge_P3 constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $x
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $y
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $z
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $t
     */
    public function __construct(
        ParagonIE_Sodium_Core32_Curve25519_Fe $x = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $y = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $z = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $t = null
    ) {
        if ($x === null) {
            $x = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        }
        $this->X = $x;
        if ($y === null) {
            $y = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        }
        $this->Y = $y;
        if ($z === null) {
            $z = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        }
        $this->Z = $z;
        if ($t === null) {
            $t = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        }
        $this->T = $t;
    }
}
Core32/Curve25519/Ge/P1p1.php000064400000003344152331075510011066 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1', false)) {
    return;
}
/**
 * Class ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1
 */
class ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1
{
    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $X;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $Y;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $Z;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $T;

    /**
     * ParagonIE_Sodium_Core32_Curve25519_Ge_P1p1 constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $x
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $y
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $z
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe|null $t
     *
     * @throws SodiumException
     * @throws TypeError
     */
    public function __construct(
        ParagonIE_Sodium_Core32_Curve25519_Fe $x = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $y = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $z = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $t = null
    ) {
        if ($x === null) {
            $x = ParagonIE_Sodium_Core32_Curve25519::fe_0();
        }
        $this->X = $x;
        if ($y === null) {
            $y = ParagonIE_Sodium_Core32_Curve25519::fe_0();
        }
        $this->Y = $y;
        if ($z === null) {
            $z = ParagonIE_Sodium_Core32_Curve25519::fe_0();
        }
        $this->Z = $z;
        if ($t === null) {
            $t = ParagonIE_Sodium_Core32_Curve25519::fe_0();
        }
        $this->T = $t;
    }
}
Core32/Curve25519/Ge/Precomp.php000064400000002775152331075510011761 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp
 */
class ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp
{
    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $yplusx;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $yminusx;

    /**
     * @var ParagonIE_Sodium_Core32_Curve25519_Fe
     */
    public $xy2d;

    /**
     * ParagonIE_Sodium_Core32_Curve25519_Ge_Precomp constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $yplusx
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $yminusx
     * @param ParagonIE_Sodium_Core32_Curve25519_Fe $xy2d
     * @throws SodiumException
     * @throws TypeError
     */
    public function __construct(
        ParagonIE_Sodium_Core32_Curve25519_Fe $yplusx = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $yminusx = null,
        ParagonIE_Sodium_Core32_Curve25519_Fe $xy2d = null
    ) {
        if ($yplusx === null) {
            $yplusx = ParagonIE_Sodium_Core32_Curve25519::fe_0();
        }
        $this->yplusx = $yplusx;
        if ($yminusx === null) {
            $yminusx = ParagonIE_Sodium_Core32_Curve25519::fe_0();
        }
        $this->yminusx = $yminusx;
        if ($xy2d === null) {
            $xy2d = ParagonIE_Sodium_Core32_Curve25519::fe_0();
        }
        $this->xy2d = $xy2d;
    }
}
Core32/Curve25519/Fe.php000064400000012127152331075510010343 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core32_Curve25519_Fe', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_Curve25519_Fe
 *
 * This represents a Field Element
 */
class ParagonIE_Sodium_Core32_Curve25519_Fe implements ArrayAccess
{
    /**
     * @var array<int, ParagonIE_Sodium_Core32_Int32>
     */
    protected $container = array();

    /**
     * @var int
     */
    protected $size = 10;

    /**
     * @internal You should not use this directly from another application
     *
     * @param array<int, ParagonIE_Sodium_Core32_Int32> $array
     * @param bool $save_indexes
     * @return self
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fromArray($array, $save_indexes = null)
    {
        $count = count($array);
        if ($save_indexes) {
            $keys = array_keys($array);
        } else {
            $keys = range(0, $count - 1);
        }
        $array = array_values($array);

        $obj = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        if ($save_indexes) {
            for ($i = 0; $i < $count; ++$i) {
                $array[$i]->overflow = 0;
                $obj->offsetSet($keys[$i], $array[$i]);
            }
        } else {
            for ($i = 0; $i < $count; ++$i) {
                $array[$i]->overflow = 0;
                $obj->offsetSet($i, $array[$i]);
            }
        }
        return $obj;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param array<int, int> $array
     * @param bool $save_indexes
     * @return self
     * @throws SodiumException
     * @throws TypeError
     */
    public static function fromIntArray($array, $save_indexes = null)
    {
        $count = count($array);
        if ($save_indexes) {
            $keys = array_keys($array);
        } else {
            $keys = range(0, $count - 1);
        }
        $array = array_values($array);
        $set = array();
        /** @var int $i */
        /** @var int $v */
        foreach ($array as $i => $v) {
            $set[$i] = ParagonIE_Sodium_Core32_Int32::fromInt($v);
        }

        $obj = new ParagonIE_Sodium_Core32_Curve25519_Fe();
        if ($save_indexes) {
            for ($i = 0; $i < $count; ++$i) {
                $set[$i]->overflow = 0;
                $obj->offsetSet($keys[$i], $set[$i]);
            }
        } else {
            for ($i = 0; $i < $count; ++$i) {
                $set[$i]->overflow = 0;
                $obj->offsetSet($i, $set[$i]);
            }
        }
        return $obj;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param mixed $offset
     * @param mixed $value
     * @return void
     * @throws SodiumException
     * @throws TypeError
     */
    public function offsetSet($offset, $value)
    {
        if (!($value instanceof ParagonIE_Sodium_Core32_Int32)) {
            throw new InvalidArgumentException('Expected an instance of ParagonIE_Sodium_Core32_Int32');
        }
        if (is_null($offset)) {
            $this->container[] = $value;
        } else {
            ParagonIE_Sodium_Core32_Util::declareScalarType($offset, 'int', 1);
            $this->container[(int) $offset] = $value;
        }
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param mixed $offset
     * @return bool
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetExists($offset)
    {
        return isset($this->container[$offset]);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param mixed $offset
     * @return void
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetUnset($offset)
    {
        unset($this->container[$offset]);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param mixed $offset
     * @return ParagonIE_Sodium_Core32_Int32
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetGet($offset)
    {
        if (!isset($this->container[$offset])) {
            $this->container[(int) $offset] = new ParagonIE_Sodium_Core32_Int32();
        }
        /** @var ParagonIE_Sodium_Core32_Int32 $get */
        $get = $this->container[$offset];
        return $get;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @return array
     */
    public function __debugInfo()
    {
        if (empty($this->container)) {
            return array();
        }
        $c = array(
            (int) ($this->container[0]->toInt()),
            (int) ($this->container[1]->toInt()),
            (int) ($this->container[2]->toInt()),
            (int) ($this->container[3]->toInt()),
            (int) ($this->container[4]->toInt()),
            (int) ($this->container[5]->toInt()),
            (int) ($this->container[6]->toInt()),
            (int) ($this->container[7]->toInt()),
            (int) ($this->container[8]->toInt()),
            (int) ($this->container[9]->toInt())
        );
        return array(implode(', ', $c));
    }
}
Core32/Curve25519/README.md000064400000000332152331075510010552 0ustar00# Curve25519 Data Structures

These are PHP implementation of the [structs used in the ref10 curve25519 code](https://github.com/jedisct1/libsodium/blob/master/src/libsodium/include/sodium/private/curve25519_ref10.h).
Core32/ChaCha20/IetfCtx.php000064400000002737152331075510011144 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_ChaCha20_IetfCtx', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_ChaCha20_IetfCtx
 */
class ParagonIE_Sodium_Core32_ChaCha20_IetfCtx extends ParagonIE_Sodium_Core32_ChaCha20_Ctx
{
    /**
     * ParagonIE_Sodium_Core_ChaCha20_IetfCtx constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $key     ChaCha20 key.
     * @param string $iv      Initialization Vector (a.k.a. nonce).
     * @param string $counter The initial counter value.
     *                        Defaults to 4 0x00 bytes.
     * @throws InvalidArgumentException
     * @throws SodiumException
     * @throws TypeError
     */
    public function __construct($key = '', $iv = '', $counter = '')
    {
        if (self::strlen($iv) !== 12) {
            throw new InvalidArgumentException('ChaCha20 expects a 96-bit nonce in IETF mode.');
        }
        parent::__construct($key, self::substr($iv, 0, 8), $counter);

        if (!empty($counter)) {
            $this->container[12] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($counter, 0, 4));
        }
        $this->container[13] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($iv, 0, 4));
        $this->container[14] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($iv, 4, 4));
        $this->container[15] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($iv, 8, 4));
    }
}
Core32/ChaCha20/Ctx.php000064400000011270152331075510010324 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Core_ChaCha20_Ctx', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Core32_ChaCha20_Ctx
 */
class ParagonIE_Sodium_Core32_ChaCha20_Ctx extends ParagonIE_Sodium_Core32_Util implements ArrayAccess
{
    /**
     * @var SplFixedArray internally, <int, ParagonIE_Sodium_Core32_Int32>
     */
    protected $container;

    /**
     * ParagonIE_Sodium_Core_ChaCha20_Ctx constructor.
     *
     * @internal You should not use this directly from another application
     *
     * @param string $key     ChaCha20 key.
     * @param string $iv      Initialization Vector (a.k.a. nonce).
     * @param string $counter The initial counter value.
     *                        Defaults to 8 0x00 bytes.
     * @throws InvalidArgumentException
     * @throws SodiumException
     * @throws TypeError
     */
    public function __construct($key = '', $iv = '', $counter = '')
    {
        if (self::strlen($key) !== 32) {
            throw new InvalidArgumentException('ChaCha20 expects a 256-bit key.');
        }
        if (self::strlen($iv) !== 8) {
            throw new InvalidArgumentException('ChaCha20 expects a 64-bit nonce.');
        }
        $this->container = new SplFixedArray(16);

        /* "expand 32-byte k" as per ChaCha20 spec */
        $this->container[0]  = new ParagonIE_Sodium_Core32_Int32(array(0x6170, 0x7865));
        $this->container[1]  = new ParagonIE_Sodium_Core32_Int32(array(0x3320, 0x646e));
        $this->container[2]  = new ParagonIE_Sodium_Core32_Int32(array(0x7962, 0x2d32));
        $this->container[3]  = new ParagonIE_Sodium_Core32_Int32(array(0x6b20, 0x6574));

        $this->container[4]  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 0, 4));
        $this->container[5]  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 4, 4));
        $this->container[6]  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 8, 4));
        $this->container[7]  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 12, 4));
        $this->container[8]  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 16, 4));
        $this->container[9]  = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 20, 4));
        $this->container[10] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 24, 4));
        $this->container[11] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($key, 28, 4));

        if (empty($counter)) {
            $this->container[12] = new ParagonIE_Sodium_Core32_Int32();
            $this->container[13] = new ParagonIE_Sodium_Core32_Int32();
        } else {
            $this->container[12] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($counter, 0, 4));
            $this->container[13] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($counter, 4, 4));
        }
        $this->container[14] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($iv, 0, 4));
        $this->container[15] = ParagonIE_Sodium_Core32_Int32::fromReverseString(self::substr($iv, 4, 4));
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $offset
     * @param int|ParagonIE_Sodium_Core32_Int32 $value
     * @return void
     */
    public function offsetSet($offset, $value)
    {
        if (!is_int($offset)) {
            throw new InvalidArgumentException('Expected an integer');
        }
        if ($value instanceof ParagonIE_Sodium_Core32_Int32) {
            /*
        } elseif (is_int($value)) {
            $value = ParagonIE_Sodium_Core32_Int32::fromInt($value);
            */
        } else {
            throw new InvalidArgumentException('Expected an integer');
        }
        $this->container[$offset] = $value;
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $offset
     * @return bool
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetExists($offset)
    {
        return isset($this->container[$offset]);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $offset
     * @return void
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetUnset($offset)
    {
        unset($this->container[$offset]);
    }

    /**
     * @internal You should not use this directly from another application
     *
     * @param int $offset
     * @return mixed|null
     * @psalm-suppress MixedArrayOffset
     */
    public function offsetGet($offset)
    {
        return isset($this->container[$offset])
            ? $this->container[$offset]
            : null;
    }
}
SodiumException.php000064400000000236152331075510010377 0ustar00<?php

if (!class_exists('SodiumException', false)) {
    /**
     * Class SodiumException
     */
    class SodiumException extends Exception
    {

    }
}
Crypto.php000064400000153032152331075510006543 0ustar00<?php

if (class_exists('ParagonIE_Sodium_Crypto', false)) {
    return;
}

/**
 * Class ParagonIE_Sodium_Crypto
 *
 * ATTENTION!
 *
 * If you are using this library, you should be using
 * ParagonIE_Sodium_Compat in your code, not this class.
 */
abstract class ParagonIE_Sodium_Crypto
{
    const aead_chacha20poly1305_KEYBYTES = 32;
    const aead_chacha20poly1305_NSECBYTES = 0;
    const aead_chacha20poly1305_NPUBBYTES = 8;
    const aead_chacha20poly1305_ABYTES = 16;

    const aead_chacha20poly1305_IETF_KEYBYTES = 32;
    const aead_chacha20poly1305_IETF_NSECBYTES = 0;
    const aead_chacha20poly1305_IETF_NPUBBYTES = 12;
    const aead_chacha20poly1305_IETF_ABYTES = 16;

    const aead_xchacha20poly1305_IETF_KEYBYTES = 32;
    const aead_xchacha20poly1305_IETF_NSECBYTES = 0;
    const aead_xchacha20poly1305_IETF_NPUBBYTES = 24;
    const aead_xchacha20poly1305_IETF_ABYTES = 16;

    const box_curve25519xsalsa20poly1305_SEEDBYTES = 32;
    const box_curve25519xsalsa20poly1305_PUBLICKEYBYTES = 32;
    const box_curve25519xsalsa20poly1305_SECRETKEYBYTES = 32;
    const box_curve25519xsalsa20poly1305_BEFORENMBYTES = 32;
    const box_curve25519xsalsa20poly1305_NONCEBYTES = 24;
    const box_curve25519xsalsa20poly1305_MACBYTES = 16;
    const box_curve25519xsalsa20poly1305_BOXZEROBYTES = 16;
    const box_curve25519xsalsa20poly1305_ZEROBYTES = 32;

    const onetimeauth_poly1305_BYTES = 16;
    const onetimeauth_poly1305_KEYBYTES = 32;

    const secretbox_xsalsa20poly1305_KEYBYTES = 32;
    const secretbox_xsalsa20poly1305_NONCEBYTES = 24;
    const secretbox_xsalsa20poly1305_MACBYTES = 16;
    const secretbox_xsalsa20poly1305_BOXZEROBYTES = 16;
    const secretbox_xsalsa20poly1305_ZEROBYTES = 32;

    const secretbox_xchacha20poly1305_KEYBYTES = 32;
    const secretbox_xchacha20poly1305_NONCEBYTES = 24;
    const secretbox_xchacha20poly1305_MACBYTES = 16;
    const secretbox_xchacha20poly1305_BOXZEROBYTES = 16;
    const secretbox_xchacha20poly1305_ZEROBYTES = 32;

    const stream_salsa20_KEYBYTES = 32;

    /**
     * AEAD Decryption with ChaCha20-Poly1305
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $ad
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function aead_chacha20poly1305_decrypt(
        $message = '',
        $ad = '',
        $nonce = '',
        $key = ''
    ) {
        /** @var int $len - Length of message (ciphertext + MAC) */
        $len = ParagonIE_Sodium_Core_Util::strlen($message);

        /** @var int  $clen - Length of ciphertext */
        $clen = $len - self::aead_chacha20poly1305_ABYTES;

        /** @var int $adlen - Length of associated data */
        $adlen = ParagonIE_Sodium_Core_Util::strlen($ad);

        /** @var string $mac - Message authentication code */
        $mac = ParagonIE_Sodium_Core_Util::substr(
            $message,
            $clen,
            self::aead_chacha20poly1305_ABYTES
        );

        /** @var string $ciphertext - The encrypted message (sans MAC) */
        $ciphertext = ParagonIE_Sodium_Core_Util::substr($message, 0, $clen);

        /** @var string The first block of the chacha20 keystream, used as a poly1305 key */
        $block0 = ParagonIE_Sodium_Core_ChaCha20::stream(
            32,
            $nonce,
            $key
        );

        /* Recalculate the Poly1305 authentication tag (MAC): */
        $state = new ParagonIE_Sodium_Core_Poly1305_State($block0);
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
        } catch (SodiumException $ex) {
            $block0 = null;
        }
        $state->update($ad);
        $state->update(ParagonIE_Sodium_Core_Util::store64_le($adlen));
        $state->update($ciphertext);
        $state->update(ParagonIE_Sodium_Core_Util::store64_le($clen));
        $computed_mac = $state->finish();

        /* Compare the given MAC with the recalculated MAC: */
        if (!ParagonIE_Sodium_Core_Util::verify_16($computed_mac, $mac)) {
            throw new SodiumException('Invalid MAC');
        }

        // Here, we know that the MAC is valid, so we decrypt and return the plaintext
        return ParagonIE_Sodium_Core_ChaCha20::streamXorIc(
            $ciphertext,
            $nonce,
            $key,
            ParagonIE_Sodium_Core_Util::store64_le(1)
        );
    }

    /**
     * AEAD Encryption with ChaCha20-Poly1305
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $ad
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function aead_chacha20poly1305_encrypt(
        $message = '',
        $ad = '',
        $nonce = '',
        $key = ''
    ) {
        /** @var int $len - Length of the plaintext message */
        $len = ParagonIE_Sodium_Core_Util::strlen($message);

        /** @var int $adlen - Length of the associated data */
        $adlen = ParagonIE_Sodium_Core_Util::strlen($ad);

        /** @var string The first block of the chacha20 keystream, used as a poly1305 key */
        $block0 = ParagonIE_Sodium_Core_ChaCha20::stream(
            32,
            $nonce,
            $key
        );
        $state = new ParagonIE_Sodium_Core_Poly1305_State($block0);
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
        } catch (SodiumException $ex) {
            $block0 = null;
        }

        /** @var string $ciphertext - Raw encrypted data */
        $ciphertext = ParagonIE_Sodium_Core_ChaCha20::streamXorIc(
            $message,
            $nonce,
            $key,
            ParagonIE_Sodium_Core_Util::store64_le(1)
        );

        $state->update($ad);
        $state->update(ParagonIE_Sodium_Core_Util::store64_le($adlen));
        $state->update($ciphertext);
        $state->update(ParagonIE_Sodium_Core_Util::store64_le($len));
        return $ciphertext . $state->finish();
    }

    /**
     * AEAD Decryption with ChaCha20-Poly1305, IETF mode (96-bit nonce)
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $ad
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function aead_chacha20poly1305_ietf_decrypt(
        $message = '',
        $ad = '',
        $nonce = '',
        $key = ''
    ) {
        /** @var int $adlen - Length of associated data */
        $adlen = ParagonIE_Sodium_Core_Util::strlen($ad);

        /** @var int $len - Length of message (ciphertext + MAC) */
        $len = ParagonIE_Sodium_Core_Util::strlen($message);

        /** @var int  $clen - Length of ciphertext */
        $clen = $len - self::aead_chacha20poly1305_IETF_ABYTES;

        /** @var string The first block of the chacha20 keystream, used as a poly1305 key */
        $block0 = ParagonIE_Sodium_Core_ChaCha20::ietfStream(
            32,
            $nonce,
            $key
        );

        /** @var string $mac - Message authentication code */
        $mac = ParagonIE_Sodium_Core_Util::substr(
            $message,
            $len - self::aead_chacha20poly1305_IETF_ABYTES,
            self::aead_chacha20poly1305_IETF_ABYTES
        );

        /** @var string $ciphertext - The encrypted message (sans MAC) */
        $ciphertext = ParagonIE_Sodium_Core_Util::substr(
            $message,
            0,
            $len - self::aead_chacha20poly1305_IETF_ABYTES
        );

        /* Recalculate the Poly1305 authentication tag (MAC): */
        $state = new ParagonIE_Sodium_Core_Poly1305_State($block0);
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
        } catch (SodiumException $ex) {
            $block0 = null;
        }
        $state->update($ad);
        $state->update(str_repeat("\x00", ((0x10 - $adlen) & 0xf)));
        $state->update($ciphertext);
        $state->update(str_repeat("\x00", (0x10 - $clen) & 0xf));
        $state->update(ParagonIE_Sodium_Core_Util::store64_le($adlen));
        $state->update(ParagonIE_Sodium_Core_Util::store64_le($clen));
        $computed_mac = $state->finish();

        /* Compare the given MAC with the recalculated MAC: */
        if (!ParagonIE_Sodium_Core_Util::verify_16($computed_mac, $mac)) {
            throw new SodiumException('Invalid MAC');
        }

        // Here, we know that the MAC is valid, so we decrypt and return the plaintext
        return ParagonIE_Sodium_Core_ChaCha20::ietfStreamXorIc(
            $ciphertext,
            $nonce,
            $key,
            ParagonIE_Sodium_Core_Util::store64_le(1)
        );
    }

    /**
     * AEAD Encryption with ChaCha20-Poly1305, IETF mode (96-bit nonce)
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $ad
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function aead_chacha20poly1305_ietf_encrypt(
        $message = '',
        $ad = '',
        $nonce = '',
        $key = ''
    ) {
        /** @var int $len - Length of the plaintext message */
        $len = ParagonIE_Sodium_Core_Util::strlen($message);

        /** @var int $adlen - Length of the associated data */
        $adlen = ParagonIE_Sodium_Core_Util::strlen($ad);

        /** @var string The first block of the chacha20 keystream, used as a poly1305 key */
        $block0 = ParagonIE_Sodium_Core_ChaCha20::ietfStream(
            32,
            $nonce,
            $key
        );
        $state = new ParagonIE_Sodium_Core_Poly1305_State($block0);
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
        } catch (SodiumException $ex) {
            $block0 = null;
        }

        /** @var string $ciphertext - Raw encrypted data */
        $ciphertext = ParagonIE_Sodium_Core_ChaCha20::ietfStreamXorIc(
            $message,
            $nonce,
            $key,
            ParagonIE_Sodium_Core_Util::store64_le(1)
        );

        $state->update($ad);
        $state->update(str_repeat("\x00", ((0x10 - $adlen) & 0xf)));
        $state->update($ciphertext);
        $state->update(str_repeat("\x00", ((0x10 - $len) & 0xf)));
        $state->update(ParagonIE_Sodium_Core_Util::store64_le($adlen));
        $state->update(ParagonIE_Sodium_Core_Util::store64_le($len));
        return $ciphertext . $state->finish();
    }

    /**
     * AEAD Decryption with ChaCha20-Poly1305, IETF mode (96-bit nonce)
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $ad
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function aead_xchacha20poly1305_ietf_decrypt(
        $message = '',
        $ad = '',
        $nonce = '',
        $key = ''
    ) {
        $subkey = ParagonIE_Sodium_Core_HChaCha20::hChaCha20(
            ParagonIE_Sodium_Core_Util::substr($nonce, 0, 16),
            $key
        );
        $nonceLast = "\x00\x00\x00\x00" .
            ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);

        return self::aead_chacha20poly1305_ietf_decrypt($message, $ad, $nonceLast, $subkey);
    }

    /**
     * AEAD Encryption with ChaCha20-Poly1305, IETF mode (96-bit nonce)
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $ad
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function aead_xchacha20poly1305_ietf_encrypt(
        $message = '',
        $ad = '',
        $nonce = '',
        $key = ''
    ) {
        $subkey = ParagonIE_Sodium_Core_HChaCha20::hChaCha20(
            ParagonIE_Sodium_Core_Util::substr($nonce, 0, 16),
            $key
        );
        $nonceLast = "\x00\x00\x00\x00" .
            ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);

        return self::aead_chacha20poly1305_ietf_encrypt($message, $ad, $nonceLast, $subkey);
    }

    /**
     * HMAC-SHA-512-256 (a.k.a. the leftmost 256 bits of HMAC-SHA-512)
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $key
     * @return string
     * @throws TypeError
     */
    public static function auth($message, $key)
    {
        return ParagonIE_Sodium_Core_Util::substr(
            hash_hmac('sha512', $message, $key, true),
            0,
            32
        );
    }

    /**
     * HMAC-SHA-512-256 validation. Constant-time via hash_equals().
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $mac
     * @param string $message
     * @param string $key
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function auth_verify($mac, $message, $key)
    {
        return ParagonIE_Sodium_Core_Util::hashEquals(
            $mac,
            self::auth($message, $key)
        );
    }

    /**
     * X25519 key exchange followed by XSalsa20Poly1305 symmetric encryption
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $plaintext
     * @param string $nonce
     * @param string $keypair
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box($plaintext, $nonce, $keypair)
    {
        $c = self::secretbox(
            $plaintext,
            $nonce,
            self::box_beforenm(
                self::box_secretkey($keypair),
                self::box_publickey($keypair)
            )
        );
        return $c;
    }

    /**
     * X25519-XSalsa20-Poly1305 with one ephemeral X25519 keypair.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $publicKey
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_seal($message, $publicKey)
    {
        /** @var string $ephemeralKeypair */
        $ephemeralKeypair = self::box_keypair();

        /** @var string $ephemeralSK */
        $ephemeralSK = self::box_secretkey($ephemeralKeypair);

        /** @var string $ephemeralPK */
        $ephemeralPK = self::box_publickey($ephemeralKeypair);

        /** @var string $nonce */
        $nonce = self::generichash(
            $ephemeralPK . $publicKey,
            '',
            24
        );

        /** @var string $keypair - The combined keypair used in crypto_box() */
        $keypair = self::box_keypair_from_secretkey_and_publickey($ephemeralSK, $publicKey);

        /** @var string $ciphertext Ciphertext + MAC from crypto_box */
        $ciphertext = self::box($message, $nonce, $keypair);
        try {
            ParagonIE_Sodium_Compat::memzero($ephemeralKeypair);
            ParagonIE_Sodium_Compat::memzero($ephemeralSK);
            ParagonIE_Sodium_Compat::memzero($nonce);
        } catch (SodiumException $ex) {
            $ephemeralKeypair = null;
            $ephemeralSK = null;
            $nonce = null;
        }
        return $ephemeralPK . $ciphertext;
    }

    /**
     * Opens a message encrypted via box_seal().
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $keypair
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_seal_open($message, $keypair)
    {
        /** @var string $ephemeralPK */
        $ephemeralPK = ParagonIE_Sodium_Core_Util::substr($message, 0, 32);

        /** @var string $ciphertext (ciphertext + MAC) */
        $ciphertext = ParagonIE_Sodium_Core_Util::substr($message, 32);

        /** @var string $secretKey */
        $secretKey = self::box_secretkey($keypair);

        /** @var string $publicKey */
        $publicKey = self::box_publickey($keypair);

        /** @var string $nonce */
        $nonce = self::generichash(
            $ephemeralPK . $publicKey,
            '',
            24
        );

        /** @var string $keypair */
        $keypair = self::box_keypair_from_secretkey_and_publickey($secretKey, $ephemeralPK);

        /** @var string $m */
        $m = self::box_open($ciphertext, $nonce, $keypair);
        try {
            ParagonIE_Sodium_Compat::memzero($secretKey);
            ParagonIE_Sodium_Compat::memzero($ephemeralPK);
            ParagonIE_Sodium_Compat::memzero($nonce);
        } catch (SodiumException $ex) {
            $secretKey = null;
            $ephemeralPK = null;
            $nonce = null;
        }
        return $m;
    }

    /**
     * Used by crypto_box() to get the crypto_secretbox() key.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $sk
     * @param string $pk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_beforenm($sk, $pk)
    {
        return ParagonIE_Sodium_Core_HSalsa20::hsalsa20(
            str_repeat("\x00", 16),
            self::scalarmult($sk, $pk)
        );
    }

    /**
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @return string
     * @throws Exception
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_keypair()
    {
        $sKey = random_bytes(32);
        $pKey = self::scalarmult_base($sKey);
        return $sKey . $pKey;
    }

    /**
     * @param string $seed
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_seed_keypair($seed)
    {
        $sKey = ParagonIE_Sodium_Core_Util::substr(
            hash('sha512', $seed, true),
            0,
            32
        );
        $pKey = self::scalarmult_base($sKey);
        return $sKey . $pKey;
    }

    /**
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $sKey
     * @param string $pKey
     * @return string
     * @throws TypeError
     */
    public static function box_keypair_from_secretkey_and_publickey($sKey, $pKey)
    {
        return ParagonIE_Sodium_Core_Util::substr($sKey, 0, 32) .
            ParagonIE_Sodium_Core_Util::substr($pKey, 0, 32);
    }

    /**
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $keypair
     * @return string
     * @throws RangeException
     * @throws TypeError
     */
    public static function box_secretkey($keypair)
    {
        if (ParagonIE_Sodium_Core_Util::strlen($keypair) !== 64) {
            throw new RangeException(
                'Must be ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES bytes long.'
            );
        }
        return ParagonIE_Sodium_Core_Util::substr($keypair, 0, 32);
    }

    /**
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $keypair
     * @return string
     * @throws RangeException
     * @throws TypeError
     */
    public static function box_publickey($keypair)
    {
        if (ParagonIE_Sodium_Core_Util::strlen($keypair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) {
            throw new RangeException(
                'Must be ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES bytes long.'
            );
        }
        return ParagonIE_Sodium_Core_Util::substr($keypair, 32, 32);
    }

    /**
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $sKey
     * @return string
     * @throws RangeException
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_publickey_from_secretkey($sKey)
    {
        if (ParagonIE_Sodium_Core_Util::strlen($sKey) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_SECRETKEYBYTES) {
            throw new RangeException(
                'Must be ParagonIE_Sodium_Compat::CRYPTO_BOX_SECRETKEYBYTES bytes long.'
            );
        }
        return self::scalarmult_base($sKey);
    }

    /**
     * Decrypt a message encrypted with box().
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $ciphertext
     * @param string $nonce
     * @param string $keypair
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function box_open($ciphertext, $nonce, $keypair)
    {
        return self::secretbox_open(
            $ciphertext,
            $nonce,
            self::box_beforenm(
                self::box_secretkey($keypair),
                self::box_publickey($keypair)
            )
        );
    }

    /**
     * Calculate a BLAKE2b hash.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string|null $key
     * @param int $outlen
     * @return string
     * @throws RangeException
     * @throws SodiumException
     * @throws TypeError
     */
    public static function generichash($message, $key = '', $outlen = 32)
    {
        // This ensures that ParagonIE_Sodium_Core_BLAKE2b::$iv is initialized
        ParagonIE_Sodium_Core_BLAKE2b::pseudoConstructor();

        $k = null;
        if (!empty($key)) {
            /** @var SplFixedArray $k */
            $k = ParagonIE_Sodium_Core_BLAKE2b::stringToSplFixedArray($key);
            if ($k->count() > ParagonIE_Sodium_Core_BLAKE2b::KEYBYTES) {
                throw new RangeException('Invalid key size');
            }
        }

        /** @var SplFixedArray $in */
        $in = ParagonIE_Sodium_Core_BLAKE2b::stringToSplFixedArray($message);

        /** @var SplFixedArray $ctx */
        $ctx = ParagonIE_Sodium_Core_BLAKE2b::init($k, $outlen);
        ParagonIE_Sodium_Core_BLAKE2b::update($ctx, $in, $in->count());

        /** @var SplFixedArray $out */
        $out = new SplFixedArray($outlen);
        $out = ParagonIE_Sodium_Core_BLAKE2b::finish($ctx, $out);

        /** @var array<int, int> */
        $outArray = $out->toArray();
        return ParagonIE_Sodium_Core_Util::intArrayToString($outArray);
    }

    /**
     * Finalize a BLAKE2b hashing context, returning the hash.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $ctx
     * @param int $outlen
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function generichash_final($ctx, $outlen = 32)
    {
        if (!is_string($ctx)) {
            throw new TypeError('Context must be a string');
        }
        $out = new SplFixedArray($outlen);

        /** @var SplFixedArray $context */
        $context = ParagonIE_Sodium_Core_BLAKE2b::stringToContext($ctx);

        /** @var SplFixedArray $out */
        $out = ParagonIE_Sodium_Core_BLAKE2b::finish($context, $out);

        /** @var array<int, int> */
        $outArray = $out->toArray();
        return ParagonIE_Sodium_Core_Util::intArrayToString($outArray);
    }

    /**
     * Initialize a hashing context for BLAKE2b.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $key
     * @param int $outputLength
     * @return string
     * @throws RangeException
     * @throws SodiumException
     * @throws TypeError
     */
    public static function generichash_init($key = '', $outputLength = 32)
    {
        // This ensures that ParagonIE_Sodium_Core_BLAKE2b::$iv is initialized
        ParagonIE_Sodium_Core_BLAKE2b::pseudoConstructor();

        $k = null;
        if (!empty($key)) {
            $k = ParagonIE_Sodium_Core_BLAKE2b::stringToSplFixedArray($key);
            if ($k->count() > ParagonIE_Sodium_Core_BLAKE2b::KEYBYTES) {
                throw new RangeException('Invalid key size');
            }
        }

        /** @var SplFixedArray $ctx */
        $ctx = ParagonIE_Sodium_Core_BLAKE2b::init($k, $outputLength);

        return ParagonIE_Sodium_Core_BLAKE2b::contextToString($ctx);
    }

    /**
     * Initialize a hashing context for BLAKE2b.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $key
     * @param int $outputLength
     * @param string $salt
     * @param string $personal
     * @return string
     * @throws RangeException
     * @throws SodiumException
     * @throws TypeError
     */
    public static function generichash_init_salt_personal(
        $key = '',
        $outputLength = 32,
        $salt = '',
        $personal = ''
    ) {
        // This ensures that ParagonIE_Sodium_Core_BLAKE2b::$iv is initialized
        ParagonIE_Sodium_Core_BLAKE2b::pseudoConstructor();

        $k = null;
        if (!empty($key)) {
            $k = ParagonIE_Sodium_Core_BLAKE2b::stringToSplFixedArray($key);
            if ($k->count() > ParagonIE_Sodium_Core_BLAKE2b::KEYBYTES) {
                throw new RangeException('Invalid key size');
            }
        }
        if (!empty($salt)) {
            $s = ParagonIE_Sodium_Core_BLAKE2b::stringToSplFixedArray($salt);
        } else {
            $s = null;
        }
        if (!empty($salt)) {
            $p = ParagonIE_Sodium_Core_BLAKE2b::stringToSplFixedArray($personal);
        } else {
            $p = null;
        }

        /** @var SplFixedArray $ctx */
        $ctx = ParagonIE_Sodium_Core_BLAKE2b::init($k, $outputLength, $s, $p);

        return ParagonIE_Sodium_Core_BLAKE2b::contextToString($ctx);
    }

    /**
     * Update a hashing context for BLAKE2b with $message
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $ctx
     * @param string $message
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function generichash_update($ctx, $message)
    {
        // This ensures that ParagonIE_Sodium_Core_BLAKE2b::$iv is initialized
        ParagonIE_Sodium_Core_BLAKE2b::pseudoConstructor();

        /** @var SplFixedArray $context */
        $context = ParagonIE_Sodium_Core_BLAKE2b::stringToContext($ctx);

        /** @var SplFixedArray $in */
        $in = ParagonIE_Sodium_Core_BLAKE2b::stringToSplFixedArray($message);

        ParagonIE_Sodium_Core_BLAKE2b::update($context, $in, $in->count());

        return ParagonIE_Sodium_Core_BLAKE2b::contextToString($context);
    }

    /**
     * Libsodium's crypto_kx().
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $my_sk
     * @param string $their_pk
     * @param string $client_pk
     * @param string $server_pk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function keyExchange($my_sk, $their_pk, $client_pk, $server_pk)
    {
        return ParagonIE_Sodium_Compat::crypto_generichash(
            ParagonIE_Sodium_Compat::crypto_scalarmult($my_sk, $their_pk) .
            $client_pk .
            $server_pk
        );
    }

    /**
     * ECDH over Curve25519
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $sKey
     * @param string $pKey
     * @return string
     *
     * @throws SodiumException
     * @throws TypeError
     */
    public static function scalarmult($sKey, $pKey)
    {
        $q = ParagonIE_Sodium_Core_X25519::crypto_scalarmult_curve25519_ref10($sKey, $pKey);
        self::scalarmult_throw_if_zero($q);
        return $q;
    }

    /**
     * ECDH over Curve25519, using the basepoint.
     * Used to get a secret key from a public key.
     *
     * @param string $secret
     * @return string
     *
     * @throws SodiumException
     * @throws TypeError
     */
    public static function scalarmult_base($secret)
    {
        $q = ParagonIE_Sodium_Core_X25519::crypto_scalarmult_curve25519_ref10_base($secret);
        self::scalarmult_throw_if_zero($q);
        return $q;
    }

    /**
     * This throws an Error if a zero public key was passed to the function.
     *
     * @param string $q
     * @return void
     * @throws SodiumException
     * @throws TypeError
     */
    protected static function scalarmult_throw_if_zero($q)
    {
        $d = 0;
        for ($i = 0; $i < self::box_curve25519xsalsa20poly1305_SECRETKEYBYTES; ++$i) {
            $d |= ParagonIE_Sodium_Core_Util::chrToInt($q[$i]);
        }

        /* branch-free variant of === 0 */
        if (-(1 & (($d - 1) >> 8))) {
            throw new SodiumException('Zero public key is not allowed');
        }
    }

    /**
     * XSalsa20-Poly1305 authenticated symmetric-key encryption.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $plaintext
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function secretbox($plaintext, $nonce, $key)
    {
        /** @var string $subkey */
        $subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key);

        /** @var string $block0 */
        $block0 = str_repeat("\x00", 32);

        /** @var int $mlen - Length of the plaintext message */
        $mlen = ParagonIE_Sodium_Core_Util::strlen($plaintext);
        $mlen0 = $mlen;
        if ($mlen0 > 64 - self::secretbox_xsalsa20poly1305_ZEROBYTES) {
            $mlen0 = 64 - self::secretbox_xsalsa20poly1305_ZEROBYTES;
        }
        $block0 .= ParagonIE_Sodium_Core_Util::substr($plaintext, 0, $mlen0);

        /** @var string $block0 */
        $block0 = ParagonIE_Sodium_Core_Salsa20::salsa20_xor(
            $block0,
            ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8),
            $subkey
        );

        /** @var string $c */
        $c = ParagonIE_Sodium_Core_Util::substr(
            $block0,
            self::secretbox_xsalsa20poly1305_ZEROBYTES
        );
        if ($mlen > $mlen0) {
            $c .= ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
                ParagonIE_Sodium_Core_Util::substr(
                    $plaintext,
                    self::secretbox_xsalsa20poly1305_ZEROBYTES
                ),
                ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8),
                1,
                $subkey
            );
        }
        $state = new ParagonIE_Sodium_Core_Poly1305_State(
            ParagonIE_Sodium_Core_Util::substr(
                $block0,
                0,
                self::onetimeauth_poly1305_KEYBYTES
            )
        );
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
            ParagonIE_Sodium_Compat::memzero($subkey);
        } catch (SodiumException $ex) {
            $block0 = null;
            $subkey = null;
        }

        $state->update($c);

        /** @var string $c - MAC || ciphertext */
        $c = $state->finish() . $c;
        unset($state);

        return $c;
    }

    /**
     * Decrypt a ciphertext generated via secretbox().
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $ciphertext
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function secretbox_open($ciphertext, $nonce, $key)
    {
        /** @var string $mac */
        $mac = ParagonIE_Sodium_Core_Util::substr(
            $ciphertext,
            0,
            self::secretbox_xsalsa20poly1305_MACBYTES
        );

        /** @var string $c */
        $c = ParagonIE_Sodium_Core_Util::substr(
            $ciphertext,
            self::secretbox_xsalsa20poly1305_MACBYTES
        );

        /** @var int $clen */
        $clen = ParagonIE_Sodium_Core_Util::strlen($c);

        /** @var string $subkey */
        $subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key);

        /** @var string $block0 */
        $block0 = ParagonIE_Sodium_Core_Salsa20::salsa20(
            64,
            ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8),
            $subkey
        );
        $verified = ParagonIE_Sodium_Core_Poly1305::onetimeauth_verify(
            $mac,
            $c,
            ParagonIE_Sodium_Core_Util::substr($block0, 0, 32)
        );
        if (!$verified) {
            try {
                ParagonIE_Sodium_Compat::memzero($subkey);
            } catch (SodiumException $ex) {
                $subkey = null;
            }
            throw new SodiumException('Invalid MAC');
        }

        /** @var string $m - Decrypted message */
        $m = ParagonIE_Sodium_Core_Util::xorStrings(
            ParagonIE_Sodium_Core_Util::substr($block0, self::secretbox_xsalsa20poly1305_ZEROBYTES),
            ParagonIE_Sodium_Core_Util::substr($c, 0, self::secretbox_xsalsa20poly1305_ZEROBYTES)
        );
        if ($clen > self::secretbox_xsalsa20poly1305_ZEROBYTES) {
            // We had more than 1 block, so let's continue to decrypt the rest.
            $m .= ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
                ParagonIE_Sodium_Core_Util::substr(
                    $c,
                    self::secretbox_xsalsa20poly1305_ZEROBYTES
                ),
                ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8),
                1,
                (string) $subkey
            );
        }
        return $m;
    }

    /**
     * XChaCha20-Poly1305 authenticated symmetric-key encryption.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $plaintext
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function secretbox_xchacha20poly1305($plaintext, $nonce, $key)
    {
        /** @var string $subkey */
        $subkey = ParagonIE_Sodium_Core_HChaCha20::hChaCha20(
            ParagonIE_Sodium_Core_Util::substr($nonce, 0, 16),
            $key
        );
        $nonceLast = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);

        /** @var string $block0 */
        $block0 = str_repeat("\x00", 32);

        /** @var int $mlen - Length of the plaintext message */
        $mlen = ParagonIE_Sodium_Core_Util::strlen($plaintext);
        $mlen0 = $mlen;
        if ($mlen0 > 64 - self::secretbox_xchacha20poly1305_ZEROBYTES) {
            $mlen0 = 64 - self::secretbox_xchacha20poly1305_ZEROBYTES;
        }
        $block0 .= ParagonIE_Sodium_Core_Util::substr($plaintext, 0, $mlen0);

        /** @var string $block0 */
        $block0 = ParagonIE_Sodium_Core_ChaCha20::streamXorIc(
            $block0,
            $nonceLast,
            $subkey
        );

        /** @var string $c */
        $c = ParagonIE_Sodium_Core_Util::substr(
            $block0,
            self::secretbox_xchacha20poly1305_ZEROBYTES
        );
        if ($mlen > $mlen0) {
            $c .= ParagonIE_Sodium_Core_ChaCha20::streamXorIc(
                ParagonIE_Sodium_Core_Util::substr(
                    $plaintext,
                    self::secretbox_xchacha20poly1305_ZEROBYTES
                ),
                $nonceLast,
                $subkey,
                ParagonIE_Sodium_Core_Util::store64_le(1)
            );
        }
        $state = new ParagonIE_Sodium_Core_Poly1305_State(
            ParagonIE_Sodium_Core_Util::substr(
                $block0,
                0,
                self::onetimeauth_poly1305_KEYBYTES
            )
        );
        try {
            ParagonIE_Sodium_Compat::memzero($block0);
            ParagonIE_Sodium_Compat::memzero($subkey);
        } catch (SodiumException $ex) {
            $block0 = null;
            $subkey = null;
        }

        $state->update($c);

        /** @var string $c - MAC || ciphertext */
        $c = $state->finish() . $c;
        unset($state);

        return $c;
    }

    /**
     * Decrypt a ciphertext generated via secretbox_xchacha20poly1305().
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $ciphertext
     * @param string $nonce
     * @param string $key
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function secretbox_xchacha20poly1305_open($ciphertext, $nonce, $key)
    {
        /** @var string $mac */
        $mac = ParagonIE_Sodium_Core_Util::substr(
            $ciphertext,
            0,
            self::secretbox_xchacha20poly1305_MACBYTES
        );

        /** @var string $c */
        $c = ParagonIE_Sodium_Core_Util::substr(
            $ciphertext,
            self::secretbox_xchacha20poly1305_MACBYTES
        );

        /** @var int $clen */
        $clen = ParagonIE_Sodium_Core_Util::strlen($c);

        /** @var string $subkey */
        $subkey = ParagonIE_Sodium_Core_HChaCha20::hchacha20($nonce, $key);

        /** @var string $block0 */
        $block0 = ParagonIE_Sodium_Core_ChaCha20::stream(
            64,
            ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8),
            $subkey
        );
        $verified = ParagonIE_Sodium_Core_Poly1305::onetimeauth_verify(
            $mac,
            $c,
            ParagonIE_Sodium_Core_Util::substr($block0, 0, 32)
        );

        if (!$verified) {
            try {
                ParagonIE_Sodium_Compat::memzero($subkey);
            } catch (SodiumException $ex) {
                $subkey = null;
            }
            throw new SodiumException('Invalid MAC');
        }

        /** @var string $m - Decrypted message */
        $m = ParagonIE_Sodium_Core_Util::xorStrings(
            ParagonIE_Sodium_Core_Util::substr($block0, self::secretbox_xchacha20poly1305_ZEROBYTES),
            ParagonIE_Sodium_Core_Util::substr($c, 0, self::secretbox_xchacha20poly1305_ZEROBYTES)
        );

        if ($clen > self::secretbox_xchacha20poly1305_ZEROBYTES) {
            // We had more than 1 block, so let's continue to decrypt the rest.
            $m .= ParagonIE_Sodium_Core_ChaCha20::streamXorIc(
                ParagonIE_Sodium_Core_Util::substr(
                    $c,
                    self::secretbox_xchacha20poly1305_ZEROBYTES
                ),
                ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8),
                (string) $subkey,
                ParagonIE_Sodium_Core_Util::store64_le(1)
            );
        }
        return $m;
    }

    /**
     * @param string $key
     * @return array<int, string> Returns a state and a header.
     * @throws Exception
     * @throws SodiumException
     */
    public static function secretstream_xchacha20poly1305_init_push($key)
    {
        # randombytes_buf(out, crypto_secretstream_xchacha20poly1305_HEADERBYTES);
        $out = random_bytes(24);

        # crypto_core_hchacha20(state->k, out, k, NULL);
        $subkey = ParagonIE_Sodium_Core_HChaCha20::hChaCha20($out, $key);
        $state = new ParagonIE_Sodium_Core_SecretStream_State(
            $subkey,
            ParagonIE_Sodium_Core_Util::substr($out, 16, 8) . str_repeat("\0", 4)
        );

        # _crypto_secretstream_xchacha20poly1305_counter_reset(state);
        $state->counterReset();

        # memcpy(STATE_INONCE(state), out + crypto_core_hchacha20_INPUTBYTES,
        #        crypto_secretstream_xchacha20poly1305_INONCEBYTES);
        # memset(state->_pad, 0, sizeof state->_pad);
        return array(
            $state->toString(),
            $out
        );
    }

    /**
     * @param string $key
     * @param string $header
     * @return string Returns a state.
     * @throws Exception
     */
    public static function secretstream_xchacha20poly1305_init_pull($key, $header)
    {
        # crypto_core_hchacha20(state->k, in, k, NULL);
        $subkey = ParagonIE_Sodium_Core_HChaCha20::hChaCha20(
            ParagonIE_Sodium_Core_Util::substr($header, 0, 16),
            $key
        );
        $state = new ParagonIE_Sodium_Core_SecretStream_State(
            $subkey,
            ParagonIE_Sodium_Core_Util::substr($header, 16)
        );
        $state->counterReset();
        # memcpy(STATE_INONCE(state), in + crypto_core_hchacha20_INPUTBYTES,
        #     crypto_secretstream_xchacha20poly1305_INONCEBYTES);
        # memset(state->_pad, 0, sizeof state->_pad);
        # return 0;
        return $state->toString();
    }

    /**
     * @param string $state
     * @param string $msg
     * @param string $aad
     * @param int $tag
     * @return string
     * @throws SodiumException
     */
    public static function secretstream_xchacha20poly1305_push(&$state, $msg, $aad = '', $tag = 0)
    {
        $st = ParagonIE_Sodium_Core_SecretStream_State::fromString($state);
        # crypto_onetimeauth_poly1305_state poly1305_state;
        # unsigned char                     block[64U];
        # unsigned char                     slen[8U];
        # unsigned char                    *c;
        # unsigned char                    *mac;

        $msglen = ParagonIE_Sodium_Core_Util::strlen($msg);
        $aadlen = ParagonIE_Sodium_Core_Util::strlen($aad);

        if ((($msglen + 63) >> 6) > 0xfffffffe) {
            throw new SodiumException(
                'message cannot be larger than SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX bytes'
            );
        }

        # if (outlen_p != NULL) {
        #     *outlen_p = 0U;
        # }
        # if (mlen > crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX) {
        #     sodium_misuse();
        # }

        # crypto_stream_chacha20_ietf(block, sizeof block, state->nonce, state->k);
        # crypto_onetimeauth_poly1305_init(&poly1305_state, block);
        # sodium_memzero(block, sizeof block);
        $auth = new ParagonIE_Sodium_Core_Poly1305_State(
            ParagonIE_Sodium_Core_ChaCha20::ietfStream(32, $st->getCombinedNonce(), $st->getKey())
        );

        # crypto_onetimeauth_poly1305_update(&poly1305_state, ad, adlen);
        $auth->update($aad);

        # crypto_onetimeauth_poly1305_update(&poly1305_state, _pad0,
        #     (0x10 - adlen) & 0xf);
        $auth->update(str_repeat("\0", ((0x10 - $aadlen) & 0xf)));

        # memset(block, 0, sizeof block);
        # block[0] = tag;
        # crypto_stream_chacha20_ietf_xor_ic(block, block, sizeof block,
        #                                    state->nonce, 1U, state->k);
        $block = ParagonIE_Sodium_Core_ChaCha20::ietfStreamXorIc(
            ParagonIE_Sodium_Core_Util::intToChr($tag) . str_repeat("\0", 63),
            $st->getCombinedNonce(),
            $st->getKey(),
            ParagonIE_Sodium_Core_Util::store64_le(1)
        );

        # crypto_onetimeauth_poly1305_update(&poly1305_state, block, sizeof block);
        $auth->update($block);

        # out[0] = block[0];
        $out = $block[0];
        # c = out + (sizeof tag);
        # crypto_stream_chacha20_ietf_xor_ic(c, m, mlen, state->nonce, 2U, state->k);
        $cipher = ParagonIE_Sodium_Core_ChaCha20::ietfStreamXorIc(
            $msg,
            $st->getCombinedNonce(),
            $st->getKey(),
            ParagonIE_Sodium_Core_Util::store64_le(2)
        );

        # crypto_onetimeauth_poly1305_update(&poly1305_state, c, mlen);
        $auth->update($cipher);

        $out .= $cipher;
        unset($cipher);

        # crypto_onetimeauth_poly1305_update
        # (&poly1305_state, _pad0, (0x10 - (sizeof block) + mlen) & 0xf);
        $auth->update(str_repeat("\0", ((0x10 - 64 + $msglen) & 0xf)));

        # STORE64_LE(slen, (uint64_t) adlen);
        $slen = ParagonIE_Sodium_Core_Util::store64_le($aadlen);

        # crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen);
        $auth->update($slen);

        # STORE64_LE(slen, (sizeof block) + mlen);
        $slen = ParagonIE_Sodium_Core_Util::store64_le(64 + $msglen);

        # crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen);
        $auth->update($slen);

        # mac = c + mlen;
        # crypto_onetimeauth_poly1305_final(&poly1305_state, mac);
        $mac = $auth->finish();
        $out .= $mac;

        # sodium_memzero(&poly1305_state, sizeof poly1305_state);
        unset($auth);


        # XOR_BUF(STATE_INONCE(state), mac,
        #     crypto_secretstream_xchacha20poly1305_INONCEBYTES);
        $st->xorNonce($mac);

        # sodium_increment(STATE_COUNTER(state),
        #     crypto_secretstream_xchacha20poly1305_COUNTERBYTES);
        $st->incrementCounter();
        // Overwrite by reference:
        $state = $st->toString();

        /** @var bool $rekey */
        $rekey = ($tag & ParagonIE_Sodium_Compat::CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_REKEY) !== 0;
        # if ((tag & crypto_secretstream_xchacha20poly1305_TAG_REKEY) != 0 ||
        #     sodium_is_zero(STATE_COUNTER(state),
        #         crypto_secretstream_xchacha20poly1305_COUNTERBYTES)) {
        #     crypto_secretstream_xchacha20poly1305_rekey(state);
        # }
        if ($rekey || $st->needsRekey()) {
            // DO REKEY
            self::secretstream_xchacha20poly1305_rekey($state);
        }
        # if (outlen_p != NULL) {
        #     *outlen_p = crypto_secretstream_xchacha20poly1305_ABYTES + mlen;
        # }
        return $out;
    }

    /**
     * @param string $state
     * @param string $cipher
     * @param string $aad
     * @return bool|array{0: string, 1: int}
     * @throws SodiumException
     */
    public static function secretstream_xchacha20poly1305_pull(&$state, $cipher, $aad = '')
    {
        $st = ParagonIE_Sodium_Core_SecretStream_State::fromString($state);

        $cipherlen = ParagonIE_Sodium_Core_Util::strlen($cipher);
        #     mlen = inlen - crypto_secretstream_xchacha20poly1305_ABYTES;
        $msglen = $cipherlen - ParagonIE_Sodium_Compat::CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES;
        $aadlen = ParagonIE_Sodium_Core_Util::strlen($aad);

        #     if (mlen > crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX) {
        #         sodium_misuse();
        #     }
        if ((($msglen + 63) >> 6) > 0xfffffffe) {
            throw new SodiumException(
                'message cannot be larger than SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX bytes'
            );
        }

        #     crypto_stream_chacha20_ietf(block, sizeof block, state->nonce, state->k);
        #     crypto_onetimeauth_poly1305_init(&poly1305_state, block);
        #     sodium_memzero(block, sizeof block);
        $auth = new ParagonIE_Sodium_Core_Poly1305_State(
            ParagonIE_Sodium_Core_ChaCha20::ietfStream(32, $st->getCombinedNonce(), $st->getKey())
        );

        #     crypto_onetimeauth_poly1305_update(&poly1305_state, ad, adlen);
        $auth->update($aad);

        #     crypto_onetimeauth_poly1305_update(&poly1305_state, _pad0,
        #         (0x10 - adlen) & 0xf);
        $auth->update(str_repeat("\0", ((0x10 - $aadlen) & 0xf)));


        #     memset(block, 0, sizeof block);
        #     block[0] = in[0];
        #     crypto_stream_chacha20_ietf_xor_ic(block, block, sizeof block,
        #                                        state->nonce, 1U, state->k);
        $block = ParagonIE_Sodium_Core_ChaCha20::ietfStreamXorIc(
            $cipher[0] . str_repeat("\0", 63),
            $st->getCombinedNonce(),
            $st->getKey(),
            ParagonIE_Sodium_Core_Util::store64_le(1)
        );
        #     tag = block[0];
        #     block[0] = in[0];
        #     crypto_onetimeauth_poly1305_update(&poly1305_state, block, sizeof block);
        $tag = ParagonIE_Sodium_Core_Util::chrToInt($block[0]);
        $block[0] = $cipher[0];
        $auth->update($block);


        #     c = in + (sizeof tag);
        #     crypto_onetimeauth_poly1305_update(&poly1305_state, c, mlen);
        $auth->update(ParagonIE_Sodium_Core_Util::substr($cipher, 1, $msglen));

        #     crypto_onetimeauth_poly1305_update
        #     (&poly1305_state, _pad0, (0x10 - (sizeof block) + mlen) & 0xf);
        $auth->update(str_repeat("\0", ((0x10 - 64 + $msglen) & 0xf)));

        #     STORE64_LE(slen, (uint64_t) adlen);
        #     crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen);
        $slen = ParagonIE_Sodium_Core_Util::store64_le($aadlen);
        $auth->update($slen);

        #     STORE64_LE(slen, (sizeof block) + mlen);
        #     crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen);
        $slen = ParagonIE_Sodium_Core_Util::store64_le(64 + $msglen);
        $auth->update($slen);

        #     crypto_onetimeauth_poly1305_final(&poly1305_state, mac);
        #     sodium_memzero(&poly1305_state, sizeof poly1305_state);
        $mac = $auth->finish();

        #     stored_mac = c + mlen;
        #     if (sodium_memcmp(mac, stored_mac, sizeof mac) != 0) {
        #     sodium_memzero(mac, sizeof mac);
        #         return -1;
        #     }

        $stored = ParagonIE_Sodium_Core_Util::substr($cipher, $msglen + 1, 16);
        if (!ParagonIE_Sodium_Core_Util::hashEquals($mac, $stored)) {
            return false;
        }

        #     crypto_stream_chacha20_ietf_xor_ic(m, c, mlen, state->nonce, 2U, state->k);
        $out = ParagonIE_Sodium_Core_ChaCha20::ietfStreamXorIc(
            ParagonIE_Sodium_Core_Util::substr($cipher, 1, $msglen),
            $st->getCombinedNonce(),
            $st->getKey(),
            ParagonIE_Sodium_Core_Util::store64_le(2)
        );

        #     XOR_BUF(STATE_INONCE(state), mac,
        #         crypto_secretstream_xchacha20poly1305_INONCEBYTES);
        $st->xorNonce($mac);

        #     sodium_increment(STATE_COUNTER(state),
        #         crypto_secretstream_xchacha20poly1305_COUNTERBYTES);
        $st->incrementCounter();

        #     if ((tag & crypto_secretstream_xchacha20poly1305_TAG_REKEY) != 0 ||
        #         sodium_is_zero(STATE_COUNTER(state),
        #             crypto_secretstream_xchacha20poly1305_COUNTERBYTES)) {
        #         crypto_secretstream_xchacha20poly1305_rekey(state);
        #     }

        // Overwrite by reference:
        $state = $st->toString();

        /** @var bool $rekey */
        $rekey = ($tag & ParagonIE_Sodium_Compat::CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_REKEY) !== 0;
        if ($rekey || $st->needsRekey()) {
            // DO REKEY
            self::secretstream_xchacha20poly1305_rekey($state);
        }
        return array($out, $tag);
    }

    /**
     * @param string $state
     * @return void
     * @throws SodiumException
     */
    public static function secretstream_xchacha20poly1305_rekey(&$state)
    {
        $st = ParagonIE_Sodium_Core_SecretStream_State::fromString($state);
        # unsigned char new_key_and_inonce[crypto_stream_chacha20_ietf_KEYBYTES +
        # crypto_secretstream_xchacha20poly1305_INONCEBYTES];
        # size_t        i;
        # for (i = 0U; i < crypto_stream_chacha20_ietf_KEYBYTES; i++) {
        #     new_key_and_inonce[i] = state->k[i];
        # }
        $new_key_and_inonce = $st->getKey();

        # for (i = 0U; i < crypto_secretstream_xchacha20poly1305_INONCEBYTES; i++) {
        #     new_key_and_inonce[crypto_stream_chacha20_ietf_KEYBYTES + i] =
        #         STATE_INONCE(state)[i];
        # }
        $new_key_and_inonce .= ParagonIE_Sodium_Core_Util::substR($st->getNonce(), 0, 8);

        # crypto_stream_chacha20_ietf_xor(new_key_and_inonce, new_key_and_inonce,
        #                                 sizeof new_key_and_inonce,
        #                                 state->nonce, state->k);

        $st->rekey(ParagonIE_Sodium_Core_ChaCha20::ietfStreamXorIc(
            $new_key_and_inonce,
            $st->getCombinedNonce(),
            $st->getKey(),
            ParagonIE_Sodium_Core_Util::store64_le(0)
        ));

        # for (i = 0U; i < crypto_stream_chacha20_ietf_KEYBYTES; i++) {
        #     state->k[i] = new_key_and_inonce[i];
        # }
        # for (i = 0U; i < crypto_secretstream_xchacha20poly1305_INONCEBYTES; i++) {
        #     STATE_INONCE(state)[i] =
        #          new_key_and_inonce[crypto_stream_chacha20_ietf_KEYBYTES + i];
        # }
        # _crypto_secretstream_xchacha20poly1305_counter_reset(state);
        $st->counterReset();

        $state = $st->toString();
    }

    /**
     * Detached Ed25519 signature.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign_detached($message, $sk)
    {
        return ParagonIE_Sodium_Core_Ed25519::sign_detached($message, $sk);
    }

    /**
     * Attached Ed25519 signature. (Returns a signed message.)
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $message
     * @param string $sk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign($message, $sk)
    {
        return ParagonIE_Sodium_Core_Ed25519::sign($message, $sk);
    }

    /**
     * Opens a signed message. If valid, returns the message.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $signedMessage
     * @param string $pk
     * @return string
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign_open($signedMessage, $pk)
    {
        return ParagonIE_Sodium_Core_Ed25519::sign_open($signedMessage, $pk);
    }

    /**
     * Verify a detached signature of a given message and public key.
     *
     * @internal Do not use this directly. Use ParagonIE_Sodium_Compat.
     *
     * @param string $signature
     * @param string $message
     * @param string $pk
     * @return bool
     * @throws SodiumException
     * @throws TypeError
     */
    public static function sign_verify_detached($signature, $message, $pk)
    {
        return ParagonIE_Sodium_Core_Ed25519::verify_detached($signature, $message, $pk);
    }
}
PHP52/SplFixedArray.php000064400000010024152331075510010567 0ustar00<?php

if (class_exists('SplFixedArray')) {
    return;
}

/**
 * The SplFixedArray class provides the main functionalities of array. The
 * main differences between a SplFixedArray and a normal PHP array is that
 * the SplFixedArray is of fixed length and allows only integers within
 * the range as indexes. The advantage is that it allows a faster array
 * implementation.
 */
class SplFixedArray implements Iterator, ArrayAccess, Countable
{
    /** @var array<int, mixed> */
    private $internalArray = array();

    /** @var int $size */
    private $size = 0;

    /**
     * SplFixedArray constructor.
     * @param int $size
     */
    public function __construct($size = 0)
    {
        $this->size = $size;
        $this->internalArray = array();
    }

    /**
     * @return int
     */
    public function count()
    {
        return count($this->internalArray);
    }

    /**
     * @return array
     */
    public function toArray()
    {
        ksort($this->internalArray);
        return (array) $this->internalArray;
    }

    /**
     * @param array $array
     * @param bool $save_indexes
     * @return SplFixedArray
     * @psalm-suppress MixedAssignment
     */
    public static function fromArray(array $array, $save_indexes = true)
    {
        $self = new SplFixedArray(count($array));
        if($save_indexes) {
            foreach($array as $key => $value) {
                $self[(int) $key] = $value;
            }
        } else {
            $i = 0;
            foreach (array_values($array) as $value) {
                $self[$i] = $value;
                $i++;
            }
        }
        return $self;
    }

    /**
     * @return int
     */
    public function getSize()
    {
        return $this->size;
    }

    /**
     * @param int $size
     * @return bool
     */
    public function setSize($size)
    {
        $this->size = $size;
        return true;
    }

    /**
     * @param string|int $index
     * @return bool
     */
    public function offsetExists($index)
    {
        return array_key_exists((int) $index, $this->internalArray);
    }

    /**
     * @param string|int $index
     * @return mixed
     */
    public function offsetGet($index)
    {
        /** @psalm-suppress MixedReturnStatement */
        return $this->internalArray[(int) $index];
    }

    /**
     * @param string|int $index
     * @param mixed $newval
     * @psalm-suppress MixedAssignment
     */
    public function offsetSet($index, $newval)
    {
        $this->internalArray[(int) $index] = $newval;
    }

    /**
     * @param string|int $index
     */
    public function offsetUnset($index)
    {
        unset($this->internalArray[(int) $index]);
    }

    /**
     * Rewind iterator back to the start
     * @link https://php.net/manual/en/splfixedarray.rewind.php
     * @return void
     * @since 5.3.0
     */
    public function rewind()
    {
        reset($this->internalArray);
    }

    /**
     * Return current array entry
     * @link https://php.net/manual/en/splfixedarray.current.php
     * @return mixed The current element value.
     * @since 5.3.0
     */
    public function current()
    {
        /** @psalm-suppress MixedReturnStatement */
        return current($this->internalArray);
    }

    /**
     * Return current array index
     * @return int The current array index.
     */
    public function key()
    {
        return key($this->internalArray);
    }

    /**
     * @return void
     */
    public function next()
    {
        next($this->internalArray);
    }

    /**
     * Check whether the array contains more elements
     * @link https://php.net/manual/en/splfixedarray.valid.php
     * @return bool true if the array contains any more elements, false otherwise.
     */
    public function valid()
    {
        if (empty($this->internalArray)) {
            return false;
        }
        $result = next($this->internalArray) !== false;
        prev($this->internalArray);
        return $result;
    }

    /**
     * Do nothing.
     */
    public function __wakeup()
    {
        // NOP
    }
}Admin/Composer/Package.php000064400000004600152333011040011417 0ustar00<?php
/**
 * Returns information about the package and handles init.
 */

/**
 * This namespace isn't compatible with the PSR-4
 * which ensures that the copy in the standalone plugin will not be autoloaded.
 */
namespace Automattic\WooCommerce\Admin\Composer;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Notes;
use Automattic\WooCommerce\Admin\Notes\NotesUnavailableException;
use Automattic\WooCommerce\Internal\Admin\FeaturePlugin;

/**
 * Main package class.
 */
class Package {

	/**
	 * Version.
	 *
	 * @var string
	 */
	const VERSION = '3.3.0';

	/**
	 * Package active.
	 *
	 * @var bool
	 */
	private static $package_active = false;

	/**
	 * Active version
	 *
	 * @var bool
	 */
	private static $active_version = null;

	/**
	 * Init the package.
	 *
	 * Only initialize for WP 5.3 or greater.
	 */
	public static function init() {
		// Avoid double initialization when the feature plugin is in use.
		if ( defined( 'WC_ADMIN_VERSION_NUMBER' ) ) {
			self::$active_version = WC_ADMIN_VERSION_NUMBER;
			return;
		}

		$feature_plugin_instance = FeaturePlugin::instance();

		// Indicate to the feature plugin that the core package exists.
		if ( ! defined( 'WC_ADMIN_PACKAGE_EXISTS' ) ) {
			define( 'WC_ADMIN_PACKAGE_EXISTS', true );
		}

		self::$package_active = true;
		self::$active_version = self::VERSION;
		$feature_plugin_instance->init();

		// Unhook the custom Action Scheduler data store class in active older versions of WC Admin.
		remove_filter( 'action_scheduler_store_class', array( $feature_plugin_instance, 'replace_actionscheduler_store_class' ) );
	}

	/**
	 * Return the version of the package.
	 *
	 * @return string
	 */
	public static function get_version() {
		return self::VERSION;
	}

	/**
	 * Return the active version of WC Admin.
	 *
	 * @return string
	 */
	public static function get_active_version() {
		return self::$active_version;
	}

	/**
	 * Return whether the package is active.
	 *
	 * @return bool
	 */
	public static function is_package_active() {
		return self::$package_active;
	}

	/**
	 * Return the path to the package.
	 *
	 * @return string
	 */
	public static function get_path() {
		return dirname( __DIR__ );
	}

	/**
	 * Checks if notes have been initialized.
	 */
	private static function is_notes_initialized() {
		try {
			Notes::load_data_store();
		} catch ( NotesUnavailableException $e ) {
			return false;
		}
		return true;
	}
}
Admin/Overrides/OrderTraits.php000064400000005077152333011040012512 0ustar00<?php
/**
 * WC Admin Order Trait
 *
 * WC Admin Order Trait class that houses shared functionality across order and refund classes.
 */

namespace Automattic\WooCommerce\Admin\Overrides;

defined( 'ABSPATH' ) || exit;

/**
 * OrderTraits class.
 */
trait OrderTraits {
	/**
	 * Calculate shipping amount for line item/product as a total shipping amount ratio based on quantity.
	 *
	 * @param WC_Order_Item $item Line item from order.
	 *
	 * @return float|int
	 */
	public function get_item_shipping_amount( $item ) {
		// Shipping amount loosely based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php
		// distributed simply based on number of line items.
		$product_qty = $item->get_quantity( 'edit' );
		$order_items = $this->get_item_count();
		if ( 0 === $order_items ) {
			return 0;
		}

		$total_shipping_amount = (float) $this->get_shipping_total();

		return $total_shipping_amount / $order_items * $product_qty;
	}

	/**
	 * Calculate shipping tax amount for line item/product as a total shipping tax amount ratio based on quantity.
	 *
	 * Loosely based on code in includes/admin/meta-boxes/views/html-order-item(s).php.
	 *
	 * @todo If WC is currently not tax enabled, but it was before (or vice versa), would this work correctly?
	 *
	 * @param WC_Order_Item $item Line item from order.
	 *
	 * @return float|int
	 */
	public function get_item_shipping_tax_amount( $item ) {
		$order_items = $this->get_item_count();
		if ( 0 === $order_items ) {
			return 0;
		}

		$product_qty               = $item->get_quantity( 'edit' );
		$order_taxes               = $this->get_taxes();
		$line_items_shipping       = $this->get_items( 'shipping' );
		$total_shipping_tax_amount = 0;
		foreach ( $line_items_shipping as $item_id => $shipping_item ) {
			$tax_data = $shipping_item->get_taxes();
			if ( $tax_data ) {
				foreach ( $order_taxes as $tax_item ) {
					$tax_item_id                = $tax_item->get_rate_id();
					$tax_item_total             = isset( $tax_data['total'][ $tax_item_id ] ) ? (float) $tax_data['total'][ $tax_item_id ] : 0;
					$total_shipping_tax_amount += $tax_item_total;
				}
			}
		}
		return $total_shipping_tax_amount / $order_items * $product_qty;
	}

	/**
	 * Calculates coupon amount for specified line item/product.
	 *
	 * Coupon calculation based on woocommerce code in includes/admin/meta-boxes/views/html-order-item.php.
	 *
	 * @param WC_Order_Item $item Line item from order.
	 *
	 * @return float
	 */
	public function get_item_coupon_amount( $item ) {
		return floatval( $item->get_subtotal( 'edit' ) - $item->get_total( 'edit' ) );
	}
}
Admin/Overrides/ThemeUpgrader.php000064400000004004152333011040012771 0ustar00<?php
/**
 * Theme upgrader used in REST API response.
 */

namespace Automattic\WooCommerce\Admin\Overrides;

defined( 'ABSPATH' ) || exit;

/**
 * Admin\Overrides\ThemeUpgrader Class.
 */
class ThemeUpgrader extends \Theme_Upgrader {
	/**
	 * Install a theme package.
	 *
	 * @param string $package The full local path or URI of the package.
	 * @param array  $args {
	 *     Optional. Other arguments for installing a theme package. Default empty array.
	 *
	 *     @type bool $clear_update_cache Whether to clear the updates cache if successful.
	 *                                    Default true.
	 * }
	 *
	 * @return bool|WP_Error True if the installation was successful, false or a WP_Error object otherwise.
	 */
	public function install( $package, $args = array() ) {
		$defaults    = array(
			'clear_update_cache' => true,
		);
		$parsed_args = wp_parse_args( $args, $defaults );

		$this->init();
		$this->install_strings();

		add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
		add_filter( 'upgrader_post_install', array( $this, 'check_parent_theme_filter' ), 10, 3 );
		if ( $parsed_args['clear_update_cache'] ) {
			// Clear cache so wp_update_themes() knows about the new theme.
			add_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9, 0 );
		}

		$result = $this->run(
			array(
				'package'           => $package,
				'destination'       => get_theme_root(),
				'clear_destination' => false, // Do not overwrite files.
				'clear_working'     => true,
				'hook_extra'        => array(
					'type'   => 'theme',
					'action' => 'install',
				),
			)
		);

		remove_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9 );
		remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
		remove_filter( 'upgrader_post_install', array( $this, 'check_parent_theme_filter' ) );

		if ( $result && ! is_wp_error( $result ) ) {
			// Refresh the Theme Update information.
			wp_clean_themes_cache( $parsed_args['clear_update_cache'] );
		}

		return $result;
	}
}
Admin/Overrides/Order.php000064400000007125152333011040011317 0ustar00<?php
/**
 * WC Admin Order
 *
 * WC Admin Order class that adds some functionality on top of general WooCommerce WC_Order.
 */

namespace Automattic\WooCommerce\Admin\Overrides;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;

/**
 * WC_Order subclass.
 */
class Order extends \WC_Order {
	/**
	 * Order traits.
	 */
	use OrderTraits;

	/**
	 * Holds refund amounts and quantities for the order.
	 *
	 * @var void|array
	 */
	protected $refunded_line_items;

	/**
	 * Caches the customer ID.
	 *
	 * @var int
	 */
	public $customer_id = null;

	/**
	 * Get only core class data in array format.
	 *
	 * @return array
	 */
	public function get_data_without_line_items() {
		return array_merge(
			array(
				'id' => $this->get_id(),
			),
			$this->data,
			array(
				'number'    => $this->get_order_number(),
				'meta_data' => $this->get_meta_data(),
			)
		);
	}

	/**
	 * Get order line item data by type.
	 *
	 * @param string $type Order line item type.
	 * @return array|bool Array of line items on success, boolean false on failure.
	 */
	public function get_line_item_data( $type ) {
		$type_to_items = array(
			'line_items'     => 'line_item',
			'tax_lines'      => 'tax',
			'shipping_lines' => 'shipping',
			'fee_lines'      => 'fee',
			'coupon_lines'   => 'coupon',
		);

		if ( isset( $type_to_items[ $type ] ) ) {
			return $this->get_items( $type_to_items[ $type ] );
		}

		return false;
	}

	/**
	 * Add filter(s) required to hook this class to substitute WC_Order.
	 */
	public static function add_filters() {
		add_filter( 'woocommerce_order_class', array( __CLASS__, 'order_class_name' ), 10, 3 );
	}

	/**
	 * Filter function to swap class WC_Order for this one in cases when it's suitable.
	 *
	 * @param string $classname Name of the class to be created.
	 * @param string $order_type Type of order object to be created.
	 * @param number $order_id Order id to create.
	 *
	 * @return string
	 */
	public static function order_class_name( $classname, $order_type, $order_id ) {
		// @todo - Only substitute class when necessary (during sync).
		if ( 'WC_Order' === $classname ) {
			return '\Automattic\WooCommerce\Admin\Overrides\Order';
		} else {
			return $classname;
		}
	}

	/**
	 * Get the customer ID used for reports in the customer lookup table.
	 *
	 * @return int
	 */
	public function get_report_customer_id() {
		if ( is_null( $this->customer_id ) ) {
			$this->customer_id = CustomersDataStore::get_or_create_customer_from_order( $this );
		}

		return $this->customer_id;
	}

	/**
	 * Returns true if the customer has made an earlier order.
	 *
	 * @return bool
	 */
	public function is_returning_customer() {
		return OrdersStatsDataStore::is_returning_customer( $this, $this->get_report_customer_id() );
	}

	/**
	 * Get the customer's first name.
	 */
	public function get_customer_first_name() {
		if ( $this->get_user_id() ) {
			return get_user_meta( $this->get_user_id(), 'first_name', true );
		}

		if ( '' !== $this->get_billing_first_name( 'edit' ) ) {
			return $this->get_billing_first_name( 'edit' );
		} else {
			return $this->get_shipping_first_name( 'edit' );
		}
	}

	/**
	 * Get the customer's last name.
	 */
	public function get_customer_last_name() {
		if ( $this->get_user_id() ) {
			return get_user_meta( $this->get_user_id(), 'last_name', true );
		}

		if ( '' !== $this->get_billing_last_name( 'edit' ) ) {
			return $this->get_billing_last_name( 'edit' );
		} else {
			return $this->get_shipping_last_name( 'edit' );
		}
	}
}
Admin/Overrides/ThemeUpgraderSkin.php000064400000001446152333011040013625 0ustar00<?php
/**
 * Theme upgrader skin used in REST API response.
 */

namespace Automattic\WooCommerce\Admin\Overrides;

defined( 'ABSPATH' ) || exit;

/**
 * Admin\Overrides\ThemeUpgraderSkin Class.
 */
class ThemeUpgraderSkin extends \Theme_Upgrader_Skin {
	/**
	 * Avoid undefined property error from \Theme_Upgrader::check_parent_theme_filter().
	 *
	 * @var array
	 */
	public $api;

	/**
	 * Hide the skin header display.
	 */
	public function header() {}

	/**
	 * Hide the skin footer display.
	 */
	public function footer() {}

	/**
	 * Hide the skin feedback display.
	 *
	 * @param string $string String to display.
	 * @param mixed  ...$args Optional text replacements.
	 */
	public function feedback( $string, ...$args ) {}

	/**
	 * Hide the skin after display.
	 */
	public function after() {}
}
Admin/Overrides/OrderRefund.php000064400000004035152333011040012460 0ustar00<?php
/**
 * WC Admin Order Refund
 *
 * WC Admin Order Refund class that adds some functionality on top of general WooCommerce WC_Order_Refund.
 */

namespace Automattic\WooCommerce\Admin\Overrides;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;

/**
 * WC_Order_Refund subclass.
 */
class OrderRefund extends \WC_Order_Refund {
	/**
	 * Order traits.
	 */
	use OrderTraits;

	/**
	 * Caches the customer ID.
	 *
	 * @var int
	 */
	public $customer_id = null;

	/**
	 * Add filter(s) required to hook this class to substitute WC_Order_Refund.
	 */
	public static function add_filters() {
		add_filter( 'woocommerce_order_class', array( __CLASS__, 'order_class_name' ), 10, 3 );
	}

	/**
	 * Filter function to swap class WC_Order_Refund for this one in cases when it's suitable.
	 *
	 * @param string $classname Name of the class to be created.
	 * @param string $order_type Type of order object to be created.
	 * @param number $order_id Order id to create.
	 *
	 * @return string
	 */
	public static function order_class_name( $classname, $order_type, $order_id ) {
		// @todo - Only substitute class when necessary (during sync).
		if ( 'WC_Order_Refund' === $classname ) {
			return '\Automattic\WooCommerce\Admin\Overrides\OrderRefund';
		} else {
			return $classname;
		}
	}

	/**
	 * Get the customer ID of the parent order used for reports in the customer lookup table.
	 *
	 * @return int|bool Customer ID of parent order, or false if parent order not found.
	 */
	public function get_report_customer_id() {
		if ( is_null( $this->customer_id ) ) {
			$parent_order = \wc_get_order( $this->get_parent_id() );

			if ( ! $parent_order ) {
				$this->customer_id = false;
			}

			$this->customer_id = CustomersDataStore::get_or_create_customer_from_order( $parent_order );
		}

		return $this->customer_id;
	}

	/**
	 * Returns null since refunds should not be counted towards returning customer counts.
	 *
	 * @return null
	 */
	public function is_returning_customer() {
		return null;
	}
}
Admin/WCAdminHelper.php000064400000005316152333011040010724 0ustar00<?php
/**
 * WCAdminHelper
 *
 * Helper class for generic WCAdmin functions.
 */

namespace Automattic\WooCommerce\Admin;

defined( 'ABSPATH' ) || exit;

/**
 * Class WCAdminHelper
 */
class WCAdminHelper {
	/**
	 * WC Admin timestamp option name.
	 */
	const WC_ADMIN_TIMESTAMP_OPTION = 'woocommerce_admin_install_timestamp';

	const WC_ADMIN_STORE_AGE_RANGES = array(
		'week-1'    => array(
			'start' => 0,
			'end'   => WEEK_IN_SECONDS,
		),
		'week-1-4'  => array(
			'start' => WEEK_IN_SECONDS,
			'end'   => WEEK_IN_SECONDS * 4,
		),
		'month-1-3' => array(
			'start' => MONTH_IN_SECONDS,
			'end'   => MONTH_IN_SECONDS * 3,
		),
		'month-3-6' => array(
			'start' => MONTH_IN_SECONDS * 3,
			'end'   => MONTH_IN_SECONDS * 6,
		),
		'month-6+'  => array(
			'start' => MONTH_IN_SECONDS * 6,
		),
	);

	/**
	 * Get the number of seconds that the store has been active.
	 *
	 * @return number Number of seconds.
	 */
	public static function get_wcadmin_active_for_in_seconds() {
		$install_timestamp = get_option( self::WC_ADMIN_TIMESTAMP_OPTION );

		if ( ! is_numeric( $install_timestamp ) ) {
			$install_timestamp = time();
			update_option( self::WC_ADMIN_TIMESTAMP_OPTION, $install_timestamp );
		}

		return time() - $install_timestamp;
	}


	/**
	 * Test how long WooCommerce Admin has been active.
	 *
	 * @param int $seconds Time in seconds to check.
	 * @return bool Whether or not WooCommerce admin has been active for $seconds.
	 */
	public static function is_wc_admin_active_for( $seconds ) {
		$wc_admin_active_for = self::get_wcadmin_active_for_in_seconds();

		return ( $wc_admin_active_for >= $seconds );
	}

	/**
	 * Test if WooCommerce Admin has been active within a pre-defined range.
	 *
	 * @param string $range range available in WC_ADMIN_STORE_AGE_RANGES.
	 * @param int    $custom_start custom start in range.
	 * @throws \InvalidArgumentException Throws exception when invalid $range is passed in.
	 * @return bool Whether or not WooCommerce admin has been active within the range.
	 */
	public static function is_wc_admin_active_in_date_range( $range, $custom_start = null ) {
		if ( ! array_key_exists( $range, self::WC_ADMIN_STORE_AGE_RANGES ) ) {
			throw new \InvalidArgumentException(
				sprintf(
					'"%s" range is not supported, use one of: %s',
					$range,
					implode( ', ', array_keys( self::WC_ADMIN_STORE_AGE_RANGES ) )
				)
			);
		}
		$wc_admin_active_for = self::get_wcadmin_active_for_in_seconds();

		$range_data = self::WC_ADMIN_STORE_AGE_RANGES[ $range ];
		$start      = null !== $custom_start ? $custom_start : $range_data['start'];
		if ( $range_data && $wc_admin_active_for >= $start ) {
			return isset( $range_data['end'] ) ? $wc_admin_active_for < $range_data['end'] : true;
		}
		return false;
	}
}
Admin/PluginsProvider/PluginsProviderInterface.php000064400000001650152333011040016410 0ustar00<?php
/**
 * Interface for a provider for getting access to plugin queries,
 * designed to be mockable for unit tests.
 */

namespace Automattic\WooCommerce\Admin\PluginsProvider;

defined( 'ABSPATH' ) || exit;

/**
 * Plugins Provider Interface
 */
interface PluginsProviderInterface {
	/**
	 * Get an array of active plugin slugs.
	 *
	 * @return array
	 */
	public function get_active_plugin_slugs();

	/**
	 * Get plugin data.
	 *
	 * @param string $plugin Path to the plugin file relative to the plugins directory or the plugin directory name.
	 *
	 * @return array|false
	 */
	public function get_plugin_data( $plugin );

	/**
	 * Get the path to the plugin file relative to the plugins directory from the plugin slug.
	 *
	 * E.g. 'woocommerce' returns 'woocommerce/woocommerce.php'
	 *
	 * @param string $slug Plugin slug to get path for.
	 *
	 * @return string|false
	 */
	public function get_plugin_path_from_slug( $slug );
}
Admin/PluginsProvider/PluginsProvider.php000064400000003510152333011040014564 0ustar00<?php
/**
 * A provider for getting access to plugin queries.
 */

namespace Automattic\WooCommerce\Admin\PluginsProvider;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\PluginsProvider\PluginsProviderInterface;
use Automattic\WooCommerce\Admin\PluginsHelper;

/**
 * Plugins Provider.
 *
 * Uses the live PluginsHelper.
 */
class PluginsProvider implements PluginsProviderInterface {
	/**
	 * The deactivated plugin slug.
	 *
	 * @var string
	 */
	private static $deactivated_plugin_slug = '';

	/**
	 * Get an array of active plugin slugs.
	 *
	 * @return array
	 */
	public function get_active_plugin_slugs() {
		return array_filter(
			PluginsHelper::get_active_plugin_slugs(),
			function( $p ) {
				return $p !== self::$deactivated_plugin_slug;
			}
		);
	}

	/**
	 * Set the deactivated plugin. This is needed because the deactivated_plugin
	 * hook happens before the option is updated which means that getting the
	 * active plugins includes the deactivated plugin.
	 *
	 * @param string $plugin_path The path to the plugin being deactivated.
	 */
	public static function set_deactivated_plugin( $plugin_path ) {
		self::$deactivated_plugin_slug = explode( '/', $plugin_path )[0];
	}

	/**
	 * Get plugin data.
	 *
	 * @param string $plugin Path to the plugin file relative to the plugins directory or the plugin directory name.
	 *
	 * @return array|false
	 */
	public function get_plugin_data( $plugin ) {
		return PluginsHelper::get_plugin_data( $plugin );
	}

	/**
	 * Get the path to the plugin file relative to the plugins directory from the plugin slug.
	 *
	 * E.g. 'woocommerce' returns 'woocommerce/woocommerce.php'
	 *
	 * @param string $slug Plugin slug to get path for.
	 *
	 * @return string|false
	 */
	public function get_plugin_path_from_slug( $slug ) {
		return PluginsHelper::get_plugin_path_from_slug( $slug );
	}
}
Admin/ReportExporter.php000064400000014440152333011040011304 0ustar00<?php
/**
 * Handles reports CSV export.
 */

namespace Automattic\WooCommerce\Admin;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use \Automattic\WooCommerce\Admin\Schedulers\SchedulerTraits;

/**
 * ReportExporter Class.
 */
class ReportExporter {
	/**
	 * Slug to identify the scheduler.
	 *
	 * @var string
	 */
	public static $name = 'report_exporter';

	/**
	 * Scheduler traits.
	 */
	use SchedulerTraits {
		init as scheduler_init;
	}

	/**
	 * Export status option name.
	 */
	const EXPORT_STATUS_OPTION = 'woocommerce_admin_report_export_status';

	/**
	 * Export file download action.
	 */
	const DOWNLOAD_EXPORT_ACTION = 'woocommerce_admin_download_report_csv';

	/**
	 * Get all available scheduling actions.
	 * Used to determine action hook names and clear events.
	 *
	 * @return array
	 */
	public static function get_scheduler_actions() {
		return array(
			'export_report'              => 'woocommerce_admin_report_export',
			'email_report_download_link' => 'woocommerce_admin_email_report_download_link',
		);
	}

	/**
	 * Add action dependencies.
	 *
	 * @return array
	 */
	public static function get_dependencies() {
		return array(
			'email_report_download_link' => self::get_action( 'export_report' ),
		);
	}

	/**
	 * Hook in action methods.
	 */
	public static function init() {
		// Initialize scheduled action handlers.
		self::scheduler_init();

		// Report download handler.
		add_action( 'admin_init', array( __CLASS__, 'download_export_file' ) );
	}

	/**
	 * Queue up actions for a full report export.
	 *
	 * @param string $export_id Unique ID for report (timestamp expected).
	 * @param string $report_type Report type. E.g. 'customers'.
	 * @param array  $report_args Report parameters, passed to data query.
	 * @param bool   $send_email Optional. Send an email when the export is complete.
	 * @return int Number of items to export.
	 */
	public static function queue_report_export( $export_id, $report_type, $report_args = array(), $send_email = false ) {
		$exporter = new ReportCSVExporter( $report_type, $report_args );
		$exporter->prepare_data_to_export();

		$total_rows  = $exporter->get_total_rows();
		$batch_size  = $exporter->get_limit();
		$num_batches = (int) ceil( $total_rows / $batch_size );

		// Create batches, like initial import.
		$report_batch_args = array( $export_id, $report_type, $report_args );

		if ( 0 < $num_batches ) {
			self::queue_batches( 1, $num_batches, 'export_report', $report_batch_args );

			if ( $send_email ) {
				$email_action_args = array( get_current_user_id(), $export_id, $report_type );
				self::schedule_action( 'email_report_download_link', $email_action_args );
			}
		}

		return $total_rows;
	}

	/**
	 * Process a report export action.
	 *
	 * @param int    $page_number Page number for this action.
	 * @param string $export_id Unique ID for report (timestamp expected).
	 * @param string $report_type Report type. E.g. 'customers'.
	 * @param array  $report_args Report parameters, passed to data query.
	 * @return void
	 */
	public static function export_report( $page_number, $export_id, $report_type, $report_args ) {
		$report_args['page'] = $page_number;

		$exporter = new ReportCSVExporter( $report_type, $report_args );
		$exporter->set_filename( "wc-{$report_type}-report-export-{$export_id}" );
		$exporter->generate_file();

		self::update_export_percentage_complete( $report_type, $export_id, $exporter->get_percent_complete() );
	}

	/**
	 * Generate a key to reference an export status.
	 *
	 * @param string $report_type Report type. E.g. 'customers'.
	 * @param string $export_id Unique ID for report (timestamp expected).
	 * @return string Status key.
	 */
	protected static function get_status_key( $report_type, $export_id ) {
		return $report_type . ':' . $export_id;
	}

	/**
	 * Update the completion percentage of a report export.
	 *
	 * @param string $report_type Report type. E.g. 'customers'.
	 * @param string $export_id Unique ID for report (timestamp expected).
	 * @param int    $percentage Completion percentage.
	 * @return void
	 */
	public static function update_export_percentage_complete( $report_type, $export_id, $percentage ) {
		$exports_status = get_option( self::EXPORT_STATUS_OPTION, array() );
		$status_key     = self::get_status_key( $report_type, $export_id );

		$exports_status[ $status_key ] = $percentage;

		update_option( self::EXPORT_STATUS_OPTION, $exports_status );
	}

	/**
	 * Get the completion percentage of a report export.
	 *
	 * @param string $report_type Report type. E.g. 'customers'.
	 * @param string $export_id Unique ID for report (timestamp expected).
	 * @return bool|int Completion percentage, or false if export not found.
	 */
	public static function get_export_percentage_complete( $report_type, $export_id ) {
		$exports_status = get_option( self::EXPORT_STATUS_OPTION, array() );
		$status_key     = self::get_status_key( $report_type, $export_id );

		if ( isset( $exports_status[ $status_key ] ) ) {
			return $exports_status[ $status_key ];
		}

		return false;
	}

	/**
	 * Serve the export file.
	 */
	public static function download_export_file() {
		// @todo - add nonce? (nonces are good for 24 hours)
		if (
			isset( $_GET['action'] ) &&
			! empty( $_GET['filename'] ) &&
			self::DOWNLOAD_EXPORT_ACTION === wp_unslash( $_GET['action'] ) && // WPCS: input var ok, sanitization ok.
			current_user_can( 'view_woocommerce_reports' )
		) {
			$exporter = new ReportCSVExporter();
			$exporter->set_filename( wp_unslash( $_GET['filename'] ) ); // WPCS: input var ok, sanitization ok.
			$exporter->export();
		}
	}

	/**
	 * Process a report export email action.
	 *
	 * @param int    $user_id User ID that requested the email.
	 * @param string $export_id Unique ID for report (timestamp expected).
	 * @param string $report_type Report type. E.g. 'customers'.
	 * @return void
	 */
	public static function email_report_download_link( $user_id, $export_id, $report_type ) {
		$percent_complete = self::get_export_percentage_complete( $report_type, $export_id );

		if ( 100 === $percent_complete ) {
			$query_args   = array(
				'action'   => self::DOWNLOAD_EXPORT_ACTION,
				'filename' => "wc-{$report_type}-report-export-{$export_id}",
			);
			$download_url = add_query_arg( $query_args, admin_url() );

			\WC_Emails::instance();
			$email = new ReportCSVEmail();
			$email->trigger( $user_id, $report_type, $download_url );
		}
	}
}
Admin/ReportCSVEmail.php000064400000007114152333011040011077 0ustar00<?php
/**
 * Handles emailing users CSV Export download links.
 */

namespace Automattic\WooCommerce\Admin;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Include dependencies.
 */
if ( ! class_exists( 'WC_Email', false ) ) {
	include_once WC_ABSPATH . 'includes/emails/class-wc-email.php';
}

/**
 * ReportCSVEmail Class.
 */
class ReportCSVEmail extends \WC_Email {
	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id             = 'admin_report_export_download';
		$this->template_base  = WC()->plugin_path() . '/includes/react-admin/emails/';
		$this->template_html  = 'html-admin-report-export-download.php';
		$this->template_plain = 'plain-admin-report-export-download.php';
		$this->report_labels  = array(
			'categories' => __( 'Categories', 'woocommerce' ),
			'coupons'    => __( 'Coupons', 'woocommerce' ),
			'customers'  => __( 'Customers', 'woocommerce' ),
			'downloads'  => __( 'Downloads', 'woocommerce' ),
			'orders'     => __( 'Orders', 'woocommerce' ),
			'products'   => __( 'Products', 'woocommerce' ),
			'revenue'    => __( 'Revenue', 'woocommerce' ),
			'stock'      => __( 'Stock', 'woocommerce' ),
			'taxes'      => __( 'Taxes', 'woocommerce' ),
			'variations' => __( 'Variations', 'woocommerce' ),
		);

		// Call parent constructor.
		parent::__construct();
	}

	/**
	 * This email has no user-facing settings.
	 */
	public function init_form_fields() {}

	/**
	 * This email has no user-facing settings.
	 */
	public function init_settings() {}

	/**
	 * Return email type.
	 *
	 * @return string
	 */
	public function get_email_type() {
		return class_exists( 'DOMDocument' ) ? 'html' : 'plain';
	}

	/**
	 * Get email heading.
	 *
	 * @return string
	 */
	public function get_default_heading() {
		return __( 'Your Report Download', 'woocommerce' );
	}

	/**
	 * Get email subject.
	 *
	 * @return string
	 */
	public function get_default_subject() {
		return __( '[{site_title}]: Your {report_name} Report download is ready', 'woocommerce' );
	}

	/**
	 * Get content html.
	 *
	 * @return string
	 */
	public function get_content_html() {
		return wc_get_template_html(
			$this->template_html,
			array(
				'report_name'   => $this->report_type,
				'download_url'  => $this->download_url,
				'email_heading' => $this->get_heading(),
				'sent_to_admin' => true,
				'plain_text'    => false,
				'email'         => $this,
			),
			'',
			$this->template_base
		);
	}

	/**
	 * Get content plain.
	 *
	 * @return string
	 */
	public function get_content_plain() {
		return wc_get_template_html(
			$this->template_plain,
			array(
				'report_name'   => $this->report_type,
				'download_url'  => $this->download_url,
				'email_heading' => $this->get_heading(),
				'sent_to_admin' => true,
				'plain_text'    => true,
				'email'         => $this,
			),
			'',
			$this->template_base
		);
	}

	/**
	 * Trigger the sending of this email.
	 *
	 * @param int    $user_id User ID to email.
	 * @param string $report_type The type of report export being emailed.
	 * @param string $download_url The URL for downloading the report.
	 */
	public function trigger( $user_id, $report_type, $download_url ) {
		$user               = new \WP_User( $user_id );
		$this->recipient    = $user->user_email;
		$this->download_url = $download_url;

		if ( isset( $this->report_labels[ $report_type ] ) ) {
			$this->report_type                   = $this->report_labels[ $report_type ];
			$this->placeholders['{report_name}'] = $this->report_type;
		}

		$this->send(
			$this->get_recipient(),
			$this->get_subject(),
			$this->get_content(),
			$this->get_headers(),
			$this->get_attachments()
		);
	}
}
Admin/PluginsHelper.php000064400000030103152333011040011053 0ustar00<?php
/**
 * PluginsHelper
 *
 * Helper class for the site's plugins.
 */

namespace Automattic\WooCommerce\Admin;

defined( 'ABSPATH' ) || exit;

if ( ! function_exists( 'get_plugins' ) ) {
	require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

/**
 * Class PluginsHelper
 */
class PluginsHelper {

	/**
	 * Initialize hooks.
	 */
	public static function init() {
		add_action( 'woocommerce_plugins_install_callback', array( __CLASS__, 'install_plugins' ), 10, 2 );
		add_action( 'woocommerce_plugins_activate_callback', array( __CLASS__, 'activate_plugins' ), 10, 2 );
	}

	/**
	 * Get the path to the plugin file relative to the plugins directory from the plugin slug.
	 *
	 * E.g. 'woocommerce' returns 'woocommerce/woocommerce.php'
	 *
	 * @param string $slug Plugin slug to get path for.
	 *
	 * @return string|false
	 */
	public static function get_plugin_path_from_slug( $slug ) {
		$plugins = get_plugins();

		if ( strstr( $slug, '/' ) ) {
			// The slug is already a plugin path.
			return $slug;
		}

		foreach ( $plugins as $plugin_path => $data ) {
			$path_parts = explode( '/', $plugin_path );
			if ( $path_parts[0] === $slug ) {
				return $plugin_path;
			}
		}

		return false;
	}

	/**
	 * Get an array of installed plugin slugs.
	 *
	 * @return array
	 */
	public static function get_installed_plugin_slugs() {
		return array_map(
			function( $plugin_path ) {
				$path_parts = explode( '/', $plugin_path );
				return $path_parts[0];
			},
			array_keys( get_plugins() )
		);
	}

	/**
	 * Get an array of installed plugins with their file paths as a key value pair.
	 *
	 * @return array
	 */
	public static function get_installed_plugins_paths() {
		$plugins           = get_plugins();
		$installed_plugins = array();

		foreach ( $plugins as $path => $plugin ) {
			$path_parts                 = explode( '/', $path );
			$slug                       = $path_parts[0];
			$installed_plugins[ $slug ] = $path;
		}

		return $installed_plugins;
	}

	/**
	 * Get an array of active plugin slugs.
	 *
	 * @return array
	 */
	public static function get_active_plugin_slugs() {
		return array_map(
			function( $plugin_path ) {
				$path_parts = explode( '/', $plugin_path );
				return $path_parts[0];
			},
			get_option( 'active_plugins', array() )
		);
	}

	/**
	 * Checks if a plugin is installed.
	 *
	 * @param string $plugin Path to the plugin file relative to the plugins directory or the plugin directory name.
	 *
	 * @return bool
	 */
	public static function is_plugin_installed( $plugin ) {
		$plugin_path = self::get_plugin_path_from_slug( $plugin );
		return $plugin_path ? array_key_exists( $plugin_path, get_plugins() ) : false;
	}

	/**
	 * Checks if a plugin is active.
	 *
	 * @param string $plugin Path to the plugin file relative to the plugins directory or the plugin directory name.
	 *
	 * @return bool
	 */
	public static function is_plugin_active( $plugin ) {
		$plugin_path = self::get_plugin_path_from_slug( $plugin );
		return $plugin_path ? in_array( $plugin_path, get_option( 'active_plugins', array() ), true ) : false;
	}

	/**
	 * Get plugin data.
	 *
	 * @param string $plugin Path to the plugin file relative to the plugins directory or the plugin directory name.
	 *
	 * @return array|false
	 */
	public static function get_plugin_data( $plugin ) {
		$plugin_path = self::get_plugin_path_from_slug( $plugin );
		$plugins     = get_plugins();

		return isset( $plugins[ $plugin_path ] ) ? $plugins[ $plugin_path ] : false;
	}

	/**
	 * Install an array of plugins.
	 *
	 * @param array $plugins Plugins to install.
	 * @return array
	 */
	public static function install_plugins( $plugins ) {
		/**
		 * Filter the list of plugins to install.
		 *
		 * @param array $plugins A list of the plugins to install.
		 * @since 6.4.0
		 */
		$plugins = apply_filters( 'woocommerce_admin_plugins_pre_install', $plugins );

		if ( empty( $plugins ) || ! is_array( $plugins ) ) {
			return new \WP_Error( 'woocommerce_plugins_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce' ) );
		}

		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		include_once ABSPATH . '/wp-admin/includes/admin.php';
		include_once ABSPATH . '/wp-admin/includes/plugin-install.php';
		include_once ABSPATH . '/wp-admin/includes/plugin.php';
		include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
		include_once ABSPATH . '/wp-admin/includes/class-plugin-upgrader.php';

		$existing_plugins  = self::get_installed_plugins_paths();
		$installed_plugins = array();
		$results           = array();
		$time              = array();
		$errors            = new \WP_Error();

		foreach ( $plugins as $plugin ) {
			$slug = sanitize_key( $plugin );

			if ( isset( $existing_plugins[ $slug ] ) ) {
				$installed_plugins[] = $plugin;
				continue;
			}

			$start_time = microtime( true );

			$api = plugins_api(
				'plugin_information',
				array(
					'slug'   => $slug,
					'fields' => array(
						'sections' => false,
					),
				)
			);

			if ( is_wp_error( $api ) ) {
				$properties = array(
					/* translators: %s: plugin slug (example: woocommerce-services) */
					'error_message'     => sprintf( __( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce' ), $slug ),
					'api_error_message' => $api->get_error_message(),
					'slug'              => $slug,
				);
				wc_admin_record_tracks_event( 'install_plugin_error', $properties );

				/**
				 * Action triggered when a plugin API call failed.
				 *
				 * @param string $slug The plugin slug.
				 * @param \WP_Error $api The API response.
				 * @since 6.4.0
				 */
				do_action( 'woocommerce_plugins_install_api_error', $slug, $api );

				$errors->add(
					$plugin,
					sprintf(
						/* translators: %s: plugin slug (example: woocommerce-services) */
						__( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce' ),
						$slug
					)
				);

				continue;
			}

			$upgrader           = new \Plugin_Upgrader( new \Automatic_Upgrader_Skin() );
			$result             = $upgrader->install( $api->download_link );
			$results[ $plugin ] = $result;
			$time[ $plugin ]    = round( ( microtime( true ) - $start_time ) * 1000 );

			if ( is_wp_error( $result ) || is_null( $result ) ) {
				$properties = array(
					/* translators: %s: plugin slug (example: woocommerce-services) */
					'error_message'         => sprintf( __( 'The requested plugin `%s` could not be installed.', 'woocommerce' ), $slug ),
					'slug'                  => $slug,
					'api_version'           => $api->version,
					'api_download_link'     => $api->download_link,
					'upgrader_skin_message' => implode( ',', $upgrader->skin->get_upgrade_messages() ),
					'result'                => $result,
				);
				wc_admin_record_tracks_event( 'install_plugin_error', $properties );

				/**
				 * Action triggered when a plugin installation fails.
				 *
				 * @param string $slug The plugin slug.
				 * @param object $api The plugin API object.
				 * @param \WP_Error|null $result The result of the plugin installation.
				 * @param \Plugin_Upgrader $upgrader The plugin upgrader.
				 * @since 6.4.0
				*/
				do_action( 'woocommerce_plugins_install_error', $slug, $api, $result, $upgrader );

				$errors->add(
					$plugin,
					sprintf(
						/* translators: %s: plugin slug (example: woocommerce-services) */
						__( 'The requested plugin `%s` could not be installed. Upgrader install failed.', 'woocommerce' ),
						$slug
					)
				);
				continue;
			}

			$installed_plugins[] = $plugin;
		}

		$data = array(
			'installed' => $installed_plugins,
			'results'   => $results,
			'errors'    => $errors,
			'time'      => $time,
		);

		return $data;
	}

	/**
	 * Schedule plugin installation.
	 *
	 * @param array $plugins Plugins to install.
	 * @return string Job ID.
	 */
	public static function schedule_install_plugins( $plugins ) {
		if ( empty( $plugins ) || ! is_array( $plugins ) ) {
			return new \WP_Error( 'woocommerce_plugins_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce' ), 404 );
		}

		$job_id = uniqid();
		WC()->queue()->schedule_single( time() + 5, 'woocommerce_plugins_install_callback', array( $plugins, $job_id ) );

		return $job_id;
	}

	/**
	 * Activate the requested plugins.
	 *
	 * @param array $plugins Plugins.
	 * @return WP_Error|array Plugin Status
	 */
	public static function activate_plugins( $plugins ) {
		if ( empty( $plugins ) || ! is_array( $plugins ) ) {
			return new \WP_Error( 'woocommerce_plugins_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce' ), 404 );
		}

		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		// the mollie-payments-for-woocommerce plugin calls `WP_Filesystem()` during it's activation hook, which crashes without this include.
		require_once ABSPATH . 'wp-admin/includes/file.php';

		/**
		 * Filter the list of plugins to activate.
		 *
		 * @param array $plugins A list of the plugins to activate.
		 * @since 6.4.0
		 */
		$plugins = apply_filters( 'woocommerce_admin_plugins_pre_activate', $plugins );

		$plugin_paths      = self::get_installed_plugins_paths();
		$errors            = new \WP_Error();
		$activated_plugins = array();

		foreach ( $plugins as $plugin ) {
			$slug = $plugin;
			$path = isset( $plugin_paths[ $slug ] ) ? $plugin_paths[ $slug ] : false;

			if ( ! $path ) {
				$errors->add(
					$plugin,
					/* translators: %s: plugin slug (example: woocommerce-services) */
					sprintf( __( 'The requested plugin `%s`. is not yet installed.', 'woocommerce' ), $slug )
				);
				continue;
			}

			$result = activate_plugin( $path );
			if ( ! is_plugin_active( $path ) ) {
				/**
				 * Action triggered when a plugin activation fails.
				 *
				 * @param string $slug The plugin slug.
				 * @param null|\WP_Error $result The result of the plugin activation.
				 * @since 6.4.0
				 */
				do_action( 'woocommerce_plugins_activate_error', $slug, $result );

				$errors->add(
					$plugin,
					/* translators: %s: plugin slug (example: woocommerce-services) */
					sprintf( __( 'The requested plugin `%s` could not be activated.', 'woocommerce' ), $slug )
				);
				continue;
			}

			$activated_plugins[] = $plugin;
		}

		$data = array(
			'activated' => $activated_plugins,
			'active'    => self::get_active_plugin_slugs(),
			'errors'    => $errors,
		);

		return $data;
	}

	/**
	 * Schedule plugin activation.
	 *
	 * @param array $plugins Plugins to activate.
	 * @return string Job ID.
	 */
	public static function schedule_activate_plugins( $plugins ) {
		if ( empty( $plugins ) || ! is_array( $plugins ) ) {
			return new \WP_Error( 'woocommerce_plugins_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce' ), 404 );
		}

		$job_id = uniqid();
		WC()->queue()->schedule_single( time() + 5, 'woocommerce_plugins_activate_callback', array( $plugins, $job_id ) );

		return $job_id;
	}

	/**
	 * Installation status.
	 *
	 * @param int $job_id Job ID.
	 * @return array Job data.
	 */
	public static function get_installation_status( $job_id = null ) {
		$actions = WC()->queue()->search(
			array(
				'hook'    => 'woocommerce_plugins_install_callback',
				'search'  => $job_id,
				'orderby' => 'date',
				'order'   => 'DESC',
			)
		);

		return self::get_action_data( $actions );
	}

	/**
	 * Gets the plugin data for the first action.
	 *
	 * @param array $actions Array of AS actions.
	 * @return array Array of action data.
	 */
	public static function get_action_data( $actions ) {
		$data = [];

		foreach ( $actions as $action_id => $action ) {
			$store  = new \ActionScheduler_DBStore();
			$status = $store->get_status( $action_id );
			$args   = $action->get_args();
			$data[] = array(
				'job_id'  => $args[1],
				'plugins' => $args[0],
				'status'  => $store->get_status( $action_id ),
			);
		}

		return $data;
	}

	/**
	 * Activation status.
	 *
	 * @param int $job_id Job ID.
	 * @return array Array of action data.
	 */
	public static function get_activation_status( $job_id = null ) {
		$actions = WC()->queue()->search(
			array(
				'hook'    => 'woocommerce_plugins_activate_callback',
				'search'  => $job_id,
				'orderby' => 'date',
				'order'   => 'DESC',
			)
		);

		return self::get_action_data( $actions );
	}

}
Admin/PageController.php000064400000041760152333011040011225 0ustar00<?php
/**
 * PageController
 */

namespace Automattic\WooCommerce\Admin;

use Automattic\WooCommerce\Admin\Features\Navigation\Screen;
use Automattic\WooCommerce\Internal\Admin\Loader;

defined( 'ABSPATH' ) || exit;

/**
 * PageController
 */
class PageController {
	/**
	 * App entry point.
	 */
	const APP_ENTRY_POINT = 'wc-admin';

	// JS-powered page root.
	const PAGE_ROOT = 'wc-admin';

	/**
	 * Singleton instance of self.
	 *
	 * @var PageController
	 */
	private static $instance = false;

	/**
	 * Current page ID (or false if not registered with this controller).
	 *
	 * @var string
	 */
	private $current_page = null;

	/**
	 * Registered pages
	 * Contains information (breadcrumbs, menu info) about JS powered pages and classic WooCommerce pages.
	 *
	 * @var array
	 */
	private $pages = array();

	/**
	 * We want a single instance of this class so we can accurately track registered menus and pages.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Constructor.
	 * Hooks added here should be removed in `wc_admin_initialize` via the feature plugin.
	 */
	public function __construct() {
		add_action( 'admin_menu', array( $this, 'register_page_handler' ) );
		add_action( 'admin_menu', array( $this, 'register_store_details_page' ) );

		// priority is 20 to run after https://github.com/woocommerce/woocommerce/blob/a55ae325306fc2179149ba9b97e66f32f84fdd9c/includes/admin/class-wc-admin-menus.php#L165.
		add_action( 'admin_head', array( $this, 'remove_app_entry_page_menu_item' ), 20 );
	}

	/**
	 * Connect an existing page to wc-admin.
	 *
	 * @param array $options {
	 *   Array describing the page.
	 *
	 *   @type string       id           Id to reference the page.
	 *   @type string|array title        Page title. Used in menus and breadcrumbs.
	 *   @type string|null  parent       Parent ID. Null for new top level page.
	 *   @type string       path         Path for this page. E.g. admin.php?page=wc-settings&tab=checkout
	 *   @type string       capability   Capability needed to access the page.
	 *   @type string       icon         Icon. Dashicons helper class, base64-encoded SVG, or 'none'.
	 *   @type int          position     Menu item position.
	 *   @type boolean      js_page      If this is a JS-powered page.
	 * }
	 */
	public function connect_page( $options ) {
		if ( ! is_array( $options['title'] ) ) {
			$options['title'] = array( $options['title'] );
		}

		/**
		 * Filter the options when connecting or registering a page.
		 *
		 * Use the `js_page` option to determine if registering.
		 *
		 * @param array $options {
		 *   Array describing the page.
		 *
		 *   @type string       id           Id to reference the page.
		 *   @type string|array title        Page title. Used in menus and breadcrumbs.
		 *   @type string|null  parent       Parent ID. Null for new top level page.
		 *   @type string       screen_id    The screen ID that represents the connected page. (Not required for registering).
		 *   @type string       path         Path for this page. E.g. admin.php?page=wc-settings&tab=checkout
		 *   @type string       capability   Capability needed to access the page.
		 *   @type string       icon         Icon. Dashicons helper class, base64-encoded SVG, or 'none'.
		 *   @type int          position     Menu item position.
		 *   @type boolean      js_page      If this is a JS-powered page.
		 * }
		 */
		$options = apply_filters( 'woocommerce_navigation_connect_page_options', $options );

		// @todo check for null ID, or collision.
		$this->pages[ $options['id'] ] = $options;
	}

	/**
	 * Determine the current page ID, if it was registered with this controller.
	 */
	public function determine_current_page() {
		$current_url       = '';
		$current_screen_id = $this->get_current_screen_id();

		if ( isset( $_SERVER['REQUEST_URI'] ) ) {
			$current_url = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
		}

		$current_query = wp_parse_url( $current_url, PHP_URL_QUERY );
		parse_str( $current_query, $current_pieces );
		$current_path  = empty( $current_pieces['page'] ) ? '' : $current_pieces['page'];
		$current_path .= empty( $current_pieces['path'] ) ? '' : '&path=' . $current_pieces['path'];

		foreach ( $this->pages as $page ) {
			if ( isset( $page['js_page'] ) && $page['js_page'] ) {
				// Check registered admin pages.
				if (
					$page['path'] === $current_path
				) {
					$this->current_page = $page;
					return;
				}
			} else {
				// Check connected admin pages.
				if (
					isset( $page['screen_id'] ) &&
					$page['screen_id'] === $current_screen_id
				) {
					$this->current_page = $page;
					return;
				}
			}
		}
		$this->current_page = false;
	}


	/**
	 * Get breadcrumbs for WooCommerce Admin Page navigation.
	 *
	 * @return array Navigation pieces (breadcrumbs).
	 */
	public function get_breadcrumbs() {
		$current_page = $this->get_current_page();

		// Bail if this isn't a page registered with this controller.
		if ( false === $current_page ) {
			// Filter documentation below.
			return apply_filters( 'woocommerce_navigation_get_breadcrumbs', array( '' ), $current_page );
		}

		if ( 1 === count( $current_page['title'] ) ) {
			$breadcrumbs = $current_page['title'];
		} else {
			// If this page has multiple title pieces, only link the first one.
			$breadcrumbs = array_merge(
				array(
					array( $current_page['path'], reset( $current_page['title'] ) ),
				),
				array_slice( $current_page['title'], 1 )
			);
		}

		if ( isset( $current_page['parent'] ) ) {
			$parent_id = $current_page['parent'];

			while ( $parent_id ) {
				if ( isset( $this->pages[ $parent_id ] ) ) {
					$parent = $this->pages[ $parent_id ];

					if ( 0 === strpos( $parent['path'], self::PAGE_ROOT ) ) {
						$parent['path'] = 'admin.php?page=' . $parent['path'];
					}

					array_unshift( $breadcrumbs, array( $parent['path'], reset( $parent['title'] ) ) );
					$parent_id = isset( $parent['parent'] ) ? $parent['parent'] : false;
				} else {
					$parent_id = false;
				}
			}
		}

		$woocommerce_breadcrumb = array( 'admin.php?page=' . self::PAGE_ROOT, __( 'WooCommerce', 'woocommerce' ) );

		array_unshift( $breadcrumbs, $woocommerce_breadcrumb );

		/**
		 * The navigation breadcrumbs for the current page.
		 *
		 * @param array         $breadcrumbs Navigation pieces (breadcrumbs).
		 * @param array|boolean $current_page The connected page data or false if not identified.
		 */
		return apply_filters( 'woocommerce_navigation_get_breadcrumbs', $breadcrumbs, $current_page );
	}

	/**
	 * Get the current page.
	 *
	 * @return array|boolean Current page or false if not registered with this controller.
	 */
	public function get_current_page() {
		// If 'current_screen' hasn't fired yet, the current page calculation
		// will fail which causes `false` to be returned for all subsquent calls.
		if ( ! did_action( 'current_screen' ) ) {
			_doing_it_wrong( __FUNCTION__, esc_html__( 'Current page retrieval should be called on or after the `current_screen` hook.', 'woocommerce' ), '0.16.0' );
		}

		if ( is_null( $this->current_page ) ) {
			$this->determine_current_page();
		}

		return $this->current_page;
	}


	/**
	 * Returns the current screen ID.
	 *
	 * This is slightly different from WP's get_current_screen, in that it attaches an action,
	 * so certain pages like 'add new' pages can have different breadcrumbs or handling.
	 * It also catches some more unique dynamic pages like taxonomy/attribute management.
	 *
	 * Format:
	 * - {$current_screen->action}-{$current_screen->action}-tab-section
	 * - {$current_screen->action}-{$current_screen->action}-tab
	 * - {$current_screen->action}-{$current_screen->action} if no tab is present
	 * - {$current_screen->action} if no action or tab is present
	 *
	 * @return string Current screen ID.
	 */
	public function get_current_screen_id() {
		$current_screen = get_current_screen();
		if ( ! $current_screen ) {
			// Filter documentation below.
			return apply_filters( 'woocommerce_navigation_current_screen_id', false, $current_screen );
		}

		$screen_pieces = array( $current_screen->id );

		if ( $current_screen->action ) {
			$screen_pieces[] = $current_screen->action;
		}

		if (
			! empty( $current_screen->taxonomy ) &&
			isset( $current_screen->post_type ) &&
			'product' === $current_screen->post_type
		) {
			// Editing a product attribute.
			if ( 0 === strpos( $current_screen->taxonomy, 'pa_' ) ) {
				$screen_pieces = array( 'product_page_product_attribute-edit' );
			}

			// Editing a product taxonomy term.
			if ( ! empty( $_GET['tag_ID'] ) ) {
				$screen_pieces = array( $current_screen->taxonomy );
			}
		}

		// Pages with default tab values.
		$pages_with_tabs = apply_filters(
			'woocommerce_navigation_pages_with_tabs',
			array(
				'wc-reports'  => 'orders',
				'wc-settings' => 'general',
				'wc-status'   => 'status',
				'wc-addons'   => 'browse-extensions',
			)
		);

		// Tabs that have sections as well.
		$wc_emails    = \WC_Emails::instance();
		$wc_email_ids = array_map( 'sanitize_title', array_keys( $wc_emails->get_emails() ) );

		$tabs_with_sections = apply_filters(
			'woocommerce_navigation_page_tab_sections',
			array(
				'products'          => array( '', 'inventory', 'downloadable' ),
				'shipping'          => array( '', 'options', 'classes' ),
				'checkout'          => array( 'bacs', 'cheque', 'cod', 'paypal' ),
				'email'             => $wc_email_ids,
				'advanced'          => array(
					'',
					'keys',
					'webhooks',
					'legacy_api',
					'woocommerce_com',
				),
				'browse-extensions' => array( 'helper' ),
			)
		);

		if ( ! empty( $_GET['page'] ) ) {
			$page = wc_clean( wp_unslash( $_GET['page'] ) );
			if ( in_array( $page, array_keys( $pages_with_tabs ) ) ) {
				if ( ! empty( $_GET['tab'] ) ) {
					$tab = wc_clean( wp_unslash( $_GET['tab'] ) );
				} else {
					$tab = $pages_with_tabs[ $page ];
				}

				$screen_pieces[] = $tab;

				if ( ! empty( $_GET['section'] ) ) {
					$section = wc_clean( wp_unslash( $_GET['section'] ) );
					if (
						isset( $tabs_with_sections[ $tab ] ) &&
						in_array( $section, array_keys( $tabs_with_sections[ $tab ] ) )
					) {
						$screen_pieces[] = $section;
					}
				}

				// Editing a shipping zone.
				if ( ( 'shipping' === $tab ) && isset( $_GET['zone_id'] ) ) {
					$screen_pieces[] = 'edit_zone';
				}
			}
		}

		/**
		 * The current screen id.
		 *
		 * Used for identifying pages to render the WooCommerce Admin header.
		 *
		 * @param string|boolean $screen_id The screen id or false if not identified.
		 * @param WP_Screen      $current_screen The current WP_Screen.
		 */
		return apply_filters( 'woocommerce_navigation_current_screen_id', implode( '-', $screen_pieces ), $current_screen );
	}

	/**
	 * Returns the path from an ID.
	 *
	 * @param  string $id  ID to get path for.
	 * @return string Path for the given ID, or the ID on lookup miss.
	 */
	public function get_path_from_id( $id ) {
		if ( isset( $this->pages[ $id ] ) && isset( $this->pages[ $id ]['path'] ) ) {
			return $this->pages[ $id ]['path'];
		}
		return $id;
	}

	/**
	 * Returns true if we are on a page connected to this controller.
	 *
	 * @return boolean
	 */
	public function is_connected_page() {
		$current_page = $this->get_current_page();

		if ( false === $current_page ) {
			$is_connected_page = false;
		} else {
			$is_connected_page = isset( $current_page['js_page'] ) ? ! $current_page['js_page'] : true;
		}

		// Disable embed on the block editor.
		$current_screen = did_action( 'current_screen' ) ? get_current_screen() : false;
		if ( ! empty( $current_screen ) && method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor() ) {
			$is_connected_page = false;
		}

		/**
		 * Whether or not the current page is an existing page connected to this controller.
		 *
		 * Used to determine if the WooCommerce Admin header should be rendered.
		 *
		 * @param boolean       $is_connected_page True if the current page is connected.
		 * @param array|boolean $current_page The connected page data or false if not identified.
		 */
		return apply_filters( 'woocommerce_navigation_is_connected_page', $is_connected_page, $current_page );
	}

	/**
	 * Returns true if we are on a page registed with this controller.
	 *
	 * @return boolean
	 */
	public function is_registered_page() {
		$current_page = $this->get_current_page();

		if ( false === $current_page ) {
			$is_registered_page = false;
		} else {
			$is_registered_page = isset( $current_page['js_page'] ) && $current_page['js_page'];
		}

		/**
		 * Whether or not the current page was registered with this controller.
		 *
		 * Used to determine if this is a JS-powered WooCommerce Admin page.
		 *
		 * @param boolean       $is_registered_page True if the current page was registered with this controller.
		 * @param array|boolean $current_page The registered page data or false if not identified.
		 */
		return apply_filters( 'woocommerce_navigation_is_registered_page', $is_registered_page, $current_page );
	}

	/**
	 * Adds a JS powered page to wc-admin.
	 *
	 * @param array $options {
	 *   Array describing the page.
	 *
	 *   @type string      id           Id to reference the page.
	 *   @type string      title        Page title. Used in menus and breadcrumbs.
	 *   @type string|null parent       Parent ID. Null for new top level page.
	 *   @type string      path         Path for this page, full path in app context; ex /analytics/report
	 *   @type string      capability   Capability needed to access the page.
	 *   @type string      icon         Icon. Dashicons helper class, base64-encoded SVG, or 'none'.
	 *   @type int         position     Menu item position.
	 *   @type int         order        Navigation item order.
	 * }
	 */
	public function register_page( $options ) {
		$defaults = array(
			'id'         => null,
			'parent'     => null,
			'title'      => '',
			'capability' => 'view_woocommerce_reports',
			'path'       => '',
			'icon'       => '',
			'position'   => null,
			'js_page'    => true,
		);

		$options = wp_parse_args( $options, $defaults );

		if ( 0 !== strpos( $options['path'], self::PAGE_ROOT ) ) {
			$options['path'] = self::PAGE_ROOT . '&path=' . $options['path'];
		}

		if ( is_null( $options['parent'] ) ) {
			add_menu_page(
				$options['title'],
				$options['title'],
				$options['capability'],
				$options['path'],
				array( __CLASS__, 'page_wrapper' ),
				$options['icon'],
				intval( round( $options['position'] ) )
			);
		} else {
			$parent_path = $this->get_path_from_id( $options['parent'] );
			// @todo check for null path.
			add_submenu_page(
				$parent_path,
				$options['title'],
				$options['title'],
				$options['capability'],
				$options['path'],
				array( __CLASS__, 'page_wrapper' )
			);
		}

		$this->connect_page( $options );
	}

	/**
	 * Get registered pages.
	 *
	 * @return array
	 */
	public function get_pages() {
		return $this->pages;
	}

	/**
	 * Set up a div for the app to render into.
	 */
	public static function page_wrapper() {
		Loader::page_wrapper();
	}

	/**
	 * Connects existing WooCommerce pages.
	 *
	 * @todo The entry point for the embed needs moved to this class as well.
	 */
	public function register_page_handler() {
		require_once WC_ADMIN_ABSPATH . 'includes/react-admin/connect-existing-pages.php';
	}

	/**
	 * Registers the store details (profiler) page.
	 */
	public function register_store_details_page() {
		wc_admin_register_page(
			array(
				'title'  => __( 'Setup Wizard', 'woocommerce' ),
				'parent' => '',
				'path'   => '/setup-wizard',
			)
		);
	}

	/**
	 * Remove the menu item for the app entry point page.
	 */
	public function remove_app_entry_page_menu_item() {
		global $submenu;
		// User does not have capabilites to see the submenu.
		if ( ! current_user_can( 'manage_woocommerce' ) || empty( $submenu['woocommerce'] ) ) {
			return;
		}

		$wc_admin_key = null;
		foreach ( $submenu['woocommerce'] as $submenu_key => $submenu_item ) {
			// Our app entry page menu item has no title.
			if ( is_null( $submenu_item[0] ) && self::APP_ENTRY_POINT === $submenu_item[2] ) {
				$wc_admin_key = $submenu_key;
				break;
			}
		}

		if ( ! $wc_admin_key ) {
			return;
		}

		unset( $submenu['woocommerce'][ $wc_admin_key ] );
	}

	/**
	 * Returns true if we are on a JS powered admin page or
	 * a "classic" (non JS app) powered admin page (an embedded page).
	 */
	public static function is_admin_or_embed_page() {
		return self::is_admin_page() || self::is_embed_page();
	}

	/**
	 * Returns true if we are on a JS powered admin page.
	 */
	public static function is_admin_page() {
		// phpcs:disable WordPress.Security.NonceVerification
		return isset( $_GET['page'] ) && 'wc-admin' === $_GET['page'];
		// phpcs:enable WordPress.Security.NonceVerification
	}

	/**
	 *  Returns true if we are on a "classic" (non JS app) powered admin page.
	 *
	 * TODO: See usage in `admin.php`. This needs refactored and implemented properly in core.
	 */
	public static function is_embed_page() {
		return wc_admin_is_connected_page() || ( ! self::is_admin_page() && class_exists( 'Automattic\WooCommerce\Admin\Features\Navigation\Screen' ) && Screen::is_woocommerce_page() );
	}
}
Admin/RemoteInboxNotifications/TransformerInterface.php000064400000001424152333011040017406 0ustar00<?php

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

use stdClass;

/**
 * An interface to define a transformer.
 *
 * Interface TransformerInterface
 *
 * @package Automattic\WooCommerce\Admin\RemoteInboxNotifications
 */
interface TransformerInterface {
	/**
	 * Transform given value to a different value.
	 *
	 * @param mixed         $value a value to transform.
	 * @param stdClass|null $arguments arguments.
	 * @param string|null   $default default value.
	 *
	 * @return mixed|null
	 */
	public function transform( $value, stdClass $arguments = null, $default = null);

	/**
	 * Validate Transformer arguments.
	 *
	 * @param stdClass|null $arguments arguments to validate.
	 *
	 * @return mixed
	 */
	public function validate( stdClass $arguments = null );
}
Admin/RemoteInboxNotifications/PublishBeforeTimeRuleProcessor.php000064400000002416152333011040021365 0ustar00<?php
/**
 * Rule processor for sending before a specified date/time.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\DateTimeProvider\CurrentDateTimeProvider;

/**
 * Rule processor for sending before a specified date/time.
 */
class PublishBeforeTimeRuleProcessor implements RuleProcessorInterface {
	/**
	 * Constructor.
	 *
	 * @param DateTimeProviderInterface $date_time_provider The DateTime provider.
	 */
	public function __construct( $date_time_provider = null ) {
		$this->date_time_provider = null === $date_time_provider
			? new CurrentDateTimeProvider()
			: $date_time_provider;
	}

	/**
	 * Process the rule.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool Whether the rule passes or not.
	 */
	public function process( $rule, $stored_state ) {
		return $this->date_time_provider->get_now() <= new \DateTime( $rule->publish_before );
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->publish_before ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/BaseLocationStateRuleProcessor.php000064400000002256152333011040021363 0ustar00<?php
/**
 * Rule processor that performs a comparison operation against the base
 * location - state.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor that performs a comparison operation against the base
 * location - state.
 */
class BaseLocationStateRuleProcessor implements RuleProcessorInterface {
	/**
	 * Performs a comparison operation against the base location - state.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the operation.
	 */
	public function process( $rule, $stored_state ) {
		$base_location = wc_get_base_location();
		if ( ! $base_location ) {
			return false;
		}

		return ComparisonOperation::compare(
			$base_location['state'],
			$rule->value,
			$rule->operation
		);
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->value ) ) {
			return false;
		}

		if ( ! isset( $rule->operation ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/WCAdminActiveForRuleProcessor.php000064400000003144152333011040021101 0ustar00<?php
/**
 * Rule processor for publishing if wc-admin has been active for at least the
 * given number of seconds.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor for publishing if wc-admin has been active for at least the
 * given number of seconds.
 */
class WCAdminActiveForRuleProcessor implements RuleProcessorInterface {
	/**
	 * Constructor
	 *
	 * @param object $wcadmin_active_for_provider Provides the amount of time wcadmin has been active for.
	 */
	public function __construct( $wcadmin_active_for_provider = null ) {
		$this->wcadmin_active_for_provider = null === $wcadmin_active_for_provider
			? new WCAdminActiveForProvider()
			: $wcadmin_active_for_provider;
	}

	/**
	 * Performs a comparison operation against the amount of time wc-admin has
	 * been active for in days.
	 *
	 * @param object $rule         The rule being processed.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the operation.
	 */
	public function process( $rule, $stored_state ) {
		$active_for_seconds = $this->wcadmin_active_for_provider->get_wcadmin_active_for_in_seconds();
		$rule_seconds       = $rule->days * DAY_IN_SECONDS;

		return ComparisonOperation::compare(
			$active_for_seconds,
			$rule_seconds,
			$rule->operation
		);
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->days ) ) {
			return false;
		}

		if ( ! isset( $rule->operation ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/PublishAfterTimeRuleProcessor.php000064400000002411152333011040021217 0ustar00<?php
/**
 * Rule processor for sending after a specified date/time.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\DateTimeProvider\CurrentDateTimeProvider;

/**
 * Rule processor for sending after a specified date/time.
 */
class PublishAfterTimeRuleProcessor implements RuleProcessorInterface {
	/**
	 * Constructor.
	 *
	 * @param DateTimeProviderInterface $date_time_provider The DateTime provider.
	 */
	public function __construct( $date_time_provider = null ) {
		$this->date_time_provider = null === $date_time_provider
			? new CurrentDateTimeProvider()
			: $date_time_provider;
	}

	/**
	 * Process the rule.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool Whether the rule passes or not.
	 */
	public function process( $rule, $stored_state ) {
		return $this->date_time_provider->get_now() >= new \DateTime( $rule->publish_after );
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->publish_after ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/ProductCountRuleProcessor.php000064400000002750152333011040020447 0ustar00<?php
/**
 * Rule processor that performs a comparison operation against the number of
 * products.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor that performs a comparison operation against the number of
 * products.
 */
class ProductCountRuleProcessor implements RuleProcessorInterface {
	/**
	 * Constructor.
	 *
	 * @param object $product_query The product query.
	 */
	public function __construct( $product_query = null ) {
		$this->product_query = null === $product_query
			? new \WC_Product_Query(
				array(
					'limit'    => 1,
					'paginate' => true,
					'return'   => 'ids',
					'status'   => array( 'publish' ),
				)
			)
			: $product_query;
	}

	/**
	 * Performs a comparison operation against the number of products.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the operation.
	 */
	public function process( $rule, $stored_state ) {
		$products = $this->product_query->get_products();

		return ComparisonOperation::compare(
			$products->total,
			$rule->value,
			$rule->operation
		);
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->value ) ) {
			return false;
		}

		if ( ! isset( $rule->operation ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/OrderCountRuleProcessor.php000064400000002336152333011040020102 0ustar00<?php
/**
 * Rule processor for publishing based on the number of orders.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor for publishing based on the number of orders.
 */
class OrderCountRuleProcessor implements RuleProcessorInterface {
	/**
	 * Constructor.
	 *
	 * @param object $orders_provider The orders provider.
	 */
	public function __construct( $orders_provider = null ) {
		$this->orders_provider = null === $orders_provider
			? new OrdersProvider()
			: $orders_provider;
	}

	/**
	 * Process the rule.
	 *
	 * @param object $rule         The rule to process.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool Whether the rule passes or not.
	 */
	public function process( $rule, $stored_state ) {
		$count = $this->orders_provider->get_order_count();

		return ComparisonOperation::compare(
			$count,
			$rule->value,
			$rule->operation
		);
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->value ) ) {
			return false;
		}

		if ( ! isset( $rule->operation ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/PluginVersionRuleProcessor.php000064400000003377152333011040020630 0ustar00<?php
/**
 * Rule processor for sending when the provided plugin is activated and
 * matches the specified version.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\PluginsProvider\PluginsProvider;

/**
 * Rule processor for sending when the provided plugin is activated and
 * matches the specified version.
 */
class PluginVersionRuleProcessor implements RuleProcessorInterface {
	/**
	 * Constructor.
	 *
	 * @param PluginsProviderInterface $plugins_provider The plugins provider.
	 */
	public function __construct( $plugins_provider = null ) {
		$this->plugins_provider = null === $plugins_provider
			? new PluginsProvider()
			: $plugins_provider;
	}

	/**
	 * Process the rule.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool Whether the rule passes or not.
	 */
	public function process( $rule, $stored_state ) {
		$active_plugin_slugs = $this->plugins_provider->get_active_plugin_slugs();

		if ( ! in_array( $rule->plugin, $active_plugin_slugs, true ) ) {
			return false;
		}

		$plugin_data = $this->plugins_provider->get_plugin_data( $rule->plugin );

		if ( ! $plugin_data ) {
			return false;
		}

		$plugin_version = $plugin_data['Version'];

		return version_compare( $plugin_version, $rule->version, $rule->operator );
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->plugin ) ) {
			return false;
		}

		if ( ! isset( $rule->version ) ) {
			return false;
		}

		if ( ! isset( $rule->operator ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/RuleProcessorInterface.php000064400000001221152333011040017706 0ustar00<?php
/**
 * Interface for a rule processor.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor interface
 */
interface RuleProcessorInterface {
	/**
	 * Processes a rule, returning the boolean result of the processing.
	 *
	 * @param object $rule         The rule to process.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the processing.
	 */
	public function process( $rule, $stored_state );

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule );
}
Admin/RemoteInboxNotifications/EvaluationLogger.php000064400000003360152333011040016533 0ustar00<?php

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

/**
 * Class EvaluationLogger
 *
 * @package Automattic\WooCommerce\Admin\RemoteInboxNotifications
 */
class EvaluationLogger {
	/**
	 * Slug of the spec.
	 *
	 * @var string
	 */
	private $slug;

	/**
	 * Results of rules in the given spec.
	 *
	 * @var array
	 */
	private $results = array();

	/**
	 * Logger class to use.
	 *
	 * @var WC_Logger_Interface|null
	 */
	private $logger;

	/**
	 * Logger source.
	 *
	 * @var string logger source.
	 */
	private $source = '';

	/**
	 * EvaluationLogger constructor.
	 *
	 * @param string               $slug Slug of a spec that is being evaluated.
	 * @param null                 $source Logger source.
	 * @param \WC_Logger_Interface $logger Logger class to use.
	 */
	public function __construct( $slug, $source = null, \WC_Logger_Interface $logger = null ) {
		$this->slug = $slug;
		if ( null === $logger ) {
			$logger = wc_get_logger();
		}

		if ( $source ) {
			$this->source = $source;
		}

		$this->logger = $logger;
	}

	/**
	 * Add evaluation result of a rule.
	 *
	 * @param string  $rule_type name of the rule being tested.
	 * @param boolean $result result of a given rule.
	 */
	public function add_result( $rule_type, $result ) {
		array_push(
			$this->results,
			array(
				'rule'   => $rule_type,
				'result' => $result ? 'passed' : 'failed',
			)
		);
	}

	/**
	 * Log the results.
	 */
	public function log() {
		if ( false === defined( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' ) || true !== constant( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' ) ) {
			return;
		}

		foreach ( $this->results as $result ) {
			$this->logger->debug(
				"[{$this->slug}] {$result['rule']}: {$result['result']}",
				array( 'source' => $this->source )
			);
		}
	}
}
Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php000064400000014201152333011040021373 0ustar00<?php
/**
 * Handles running specs
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\PluginsProvider\PluginsProvider;
use \Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use \Automattic\WooCommerce\Admin\Notes\Note;

/**
 * Remote Inbox Notifications engine.
 * This goes through the specs and runs (creates admin notes) for those
 * specs that are able to be triggered.
 */
class RemoteInboxNotificationsEngine {
	const STORED_STATE_OPTION_NAME = 'wc_remote_inbox_notifications_stored_state';
	const WCA_UPDATED_OPTION_NAME  = 'wc_remote_inbox_notifications_wca_updated';

	/**
	 * Initialize the engine.
	 */
	public static function init() {
		// Init things that need to happen before admin_init.
		add_action( 'init', array( __CLASS__, 'on_init' ), 0, 0 );

		// Continue init via admin_init.
		add_action( 'admin_init', array( __CLASS__, 'on_admin_init' ) );

		// Trigger when the profile data option is updated (during onboarding).
		add_action(
			'update_option_' . OnboardingProfile::DATA_OPTION,
			array( __CLASS__, 'update_profile_option' ),
			10,
			2
		);

		// Hook into WCA updated. This is hooked up here rather than in
		// on_admin_init because that runs too late to hook into the action.
		add_action(
			'woocommerce_updated',
			function() {
				$next_hook = WC()->queue()->get_next(
					'woocommerce_run_on_woocommerce_admin_updated',
					array( __CLASS__, 'run_on_woocommerce_admin_updated' ),
					'woocommerce-remote-inbox-engine'
				);
				if ( $next_hook === null ) {
					WC()->queue()->schedule_single(
						time(),
						'woocommerce_run_on_woocommerce_admin_updated',
						array( __CLASS__, 'run_on_woocommerce_admin_updated' ),
						'woocommerce-remote-inbox-engine'
					);
				}
			}
		);

		add_filter( 'woocommerce_get_note_from_db', array( __CLASS__, 'get_note_from_db' ), 10, 1 );
	}

	/**
	 * This is triggered when the profile option is updated and if the
	 * profiler is being completed, triggers a run of the engine.
	 *
	 * @param mixed $old_value Old value.
	 * @param mixed $new_value New value.
	 */
	public static function update_profile_option( $old_value, $new_value ) {
		// Return early if we're not completing the profiler.
		if (
			( isset( $old_value['completed'] ) && $old_value['completed'] ) ||
			! isset( $new_value['completed'] ) ||
			! $new_value['completed']
		) {
			return;
		}

		self::run();
	}

	/**
	 * Init is continued via admin_init so that WC is loaded when the product
	 * query is used, otherwise the query generates a "0 = 1" in the WHERE
	 * condition and thus doesn't return any results.
	 */
	public static function on_admin_init() {
		add_action( 'activated_plugin', array( __CLASS__, 'run' ) );
		add_action( 'deactivated_plugin', array( __CLASS__, 'run_on_deactivated_plugin' ), 10, 1 );
		StoredStateSetupForProducts::admin_init();

		// Pre-fetch stored state so it has the correct initial values.
		self::get_stored_state();
	}

	/**
	 * An init hook is used here so that StoredStateSetupForProducts can set
	 * up a hook that gets triggered by action-scheduler - this is needed
	 * because the admin_init hook doesn't get triggered by WP Cron.
	 */
	public static function on_init() {
		StoredStateSetupForProducts::init();
	}

	/**
	 * Go through the specs and run them.
	 */
	public static function run() {
		$specs = DataSourcePoller::get_instance()->get_specs_from_data_sources();

		if ( $specs === false || count( $specs ) === 0 ) {
			return;
		}

		$stored_state = self::get_stored_state();

		foreach ( $specs as $spec ) {
			SpecRunner::run_spec( $spec, $stored_state );
		}
	}

	/**
	 * Set an option indicating that WooCommerce Admin has just been updated,
	 * run the specs, then clear that option. This lets the
	 * WooCommerceAdminUpdatedRuleProcessor trigger on WCA update.
	 */
	public static function run_on_woocommerce_admin_updated() {
		update_option( self::WCA_UPDATED_OPTION_NAME, true, false );

		self::run();

		update_option( self::WCA_UPDATED_OPTION_NAME, false, false );
	}

	/**
	 * Gets the stored state option, and does the initial set up if it doesn't
	 * already exist.
	 *
	 * @return object The stored state option.
	 */
	public static function get_stored_state() {
		$stored_state = get_option( self::STORED_STATE_OPTION_NAME );

		if ( $stored_state === false ) {
			$stored_state = new \stdClass();

			$stored_state = StoredStateSetupForProducts::init_stored_state(
				$stored_state
			);

			add_option(
				self::STORED_STATE_OPTION_NAME,
				$stored_state,
				'',
				false
			);
		}

		return $stored_state;
	}

	/**
	 * The deactivated_plugin hook happens before the option is updated
	 * (https://github.com/WordPress/WordPress/blob/master/wp-admin/includes/plugin.php#L826)
	 * so this captures the deactivated plugin path and pushes it into the
	 * PluginsProvider.
	 *
	 * @param string $plugin Path to the plugin file relative to the plugins directory.
	 */
	public static function run_on_deactivated_plugin( $plugin ) {
		PluginsProvider::set_deactivated_plugin( $plugin );
		self::run();
	}

	/**
	 * Update the stored state option.
	 *
	 * @param object $stored_state The stored state.
	 */
	public static function update_stored_state( $stored_state ) {
		update_option( self::STORED_STATE_OPTION_NAME, $stored_state, false );
	}

	/**
	 * Get the note. This is used to display localized note.
	 *
	 * @param Note $note_from_db The note object created from db.
	 * @return Note The note.
	 */
	public static function get_note_from_db( $note_from_db ) {
		if ( ! $note_from_db instanceof Note || get_user_locale() === $note_from_db->get_locale() ) {
			return $note_from_db;
		}
		$specs = DataSourcePoller::get_instance()->get_specs_from_data_sources();
		foreach ( $specs as $spec ) {
			if ( $spec->slug !== $note_from_db->get_name() ) {
				continue;
			}
			$locale = SpecRunner::get_locale( $spec->locales, true );
			if ( $locale === null ) {
				// No locale found, so don't update the note.
				break;
			}

			$note_from_db->set_title( $locale->title );
			$note_from_db->set_content( $locale->content );
			$note_from_db->set_actions( SpecRunner::get_actions( $spec ) );
		}

		return $note_from_db;
	}
}
Admin/RemoteInboxNotifications/RuleEvaluator.php000064400000004327152333011040016062 0ustar00<?php
/**
 * Evaluate the given rules as an AND operation - return false early if a
 * rule evaluates to false.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Evaluate the given rules as an AND operation - return false early if a
 * rule evaluates to false.
 */
class RuleEvaluator {
	/**
	 * Constructor.
	 *
	 * @param GetRuleProcessor $get_rule_processor The GetRuleProcessor to use.
	 */
	public function __construct( $get_rule_processor = null ) {
		$this->get_rule_processor = null === $get_rule_processor
			? new GetRuleProcessor()
			: $get_rule_processor;
	}

	/**
	 * Evaluate the given rules as an AND operation - return false early if a
	 * rule evaluates to false.
	 *
	 * @param array|object $rules The rule or rules being processed.
	 * @param object|null  $stored_state Stored state.
	 * @param array        $logger_args Arguments for the event logger. `slug` is required.
	 *
	 * @throws \InvalidArgumentException Thrown when $logger_args is missing slug.
	 *
	 * @return bool The result of the operation.
	 */
	public function evaluate( $rules, $stored_state = null, $logger_args = array() ) {

		if ( is_bool( $rules ) ) {
			return $rules;
		}

		if ( ! is_array( $rules ) ) {
			$rules = array( $rules );
		}

		if ( 0 === count( $rules ) ) {
			return false;
		}

		$evaluation_logger = null;

		if ( count( $logger_args ) ) {
			if ( ! array_key_exists( 'slug', $logger_args ) ) {
				throw new \InvalidArgumentException( 'Missing required field: slug in $logger_args.' );
			}

			array_key_exists( 'source', $logger_args ) ? $source = $logger_args['source'] : $source = null;

			$evaluation_logger = new EvaluationLogger( $logger_args['slug'], $source );
		}

		foreach ( $rules as $rule ) {
			if ( ! is_object( $rule ) ) {
				return false;
			}

			$processor        = $this->get_rule_processor->get_processor( $rule->type );
			$processor_result = $processor->process( $rule, $stored_state );
			$evaluation_logger && $evaluation_logger->add_result( $rule->type, $processor_result );

			if ( ! $processor_result ) {
				$evaluation_logger && $evaluation_logger->log();
				return false;
			}
		}

		$evaluation_logger && $evaluation_logger->log();

		return true;
	}
}
Admin/RemoteInboxNotifications/StoredStateSetupForProducts.php000064400000007202152333011040020740 0ustar00<?php
/**
 * Handles stored state setup for products.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\PluginsProvider\PluginsProvider;
use \Automattic\WooCommerce\Admin\RemoteInboxNotifications\SpecRunner;

/**
 * Handles stored state setup for products.
 */
class StoredStateSetupForProducts {
	const ASYNC_RUN_REMOTE_NOTIFICATIONS_ACTION_NAME =
		'woocommerce_admin/stored_state_setup_for_products/async/run_remote_notifications';

	/**
	 * Initialize the class via the admin_init hook.
	 */
	public static function admin_init() {
		add_action( 'product_page_product_importer', array( __CLASS__, 'run_on_product_importer' ) );
		add_action( 'transition_post_status', array( __CLASS__, 'run_on_transition_post_status' ), 10, 3 );
	}

	/**
	 * Initialize the class via the init hook.
	 */
	public static function init() {
		add_action( self::ASYNC_RUN_REMOTE_NOTIFICATIONS_ACTION_NAME, array( __CLASS__, 'run_remote_notifications' ) );
	}

	/**
	 * Run the remote notifications engine. This is triggered by
	 * action-scheduler after a product is added. It also cleans up from
	 * setting the product count increment.
	 */
	public static function run_remote_notifications() {
		RemoteInboxNotificationsEngine::run();
	}

	/**
	 * Set initial stored state values.
	 *
	 * @param object $stored_state The stored state.
	 *
	 * @return object The stored state.
	 */
	public static function init_stored_state( $stored_state ) {
		$stored_state->there_were_no_products = ! self::are_there_products();
		$stored_state->there_are_now_products = ! $stored_state->there_were_no_products;

		return $stored_state;
	}

	/**
	 * Are there products query.
	 *
	 * @return bool
	 */
	private static function are_there_products() {
		$query    = new \WC_Product_Query(
			array(
				'limit'    => 1,
				'paginate' => true,
				'return'   => 'ids',
				'status'   => array( 'publish' ),
			)
		);
		$products = $query->get_products();
		$count    = $products->total;

		return $count > 0;
	}

	/**
	 * Runs on product importer steps.
	 */
	public static function run_on_product_importer() {
		// We're only interested in when the importer completes.
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_REQUEST['step'] ) ) {
			return;
		}
		if ( 'done' !== $_REQUEST['step'] ) {
			return;
		}
		// phpcs:enable

		self::update_stored_state_and_possibly_run_remote_notifications();
	}

	/**
	 * Runs when a post status transitions, but we're only interested if it is
	 * a product being published.
	 *
	 * @param string $new_status The new status.
	 * @param string $old_status The old status.
	 * @param Post   $post       The post.
	 */
	public static function run_on_transition_post_status( $new_status, $old_status, $post ) {
		if (
			'product' !== $post->post_type ||
			'publish' !== $new_status
		) {
			return;
		}

		self::update_stored_state_and_possibly_run_remote_notifications();
	}

	/**
	 * Enqueues an async action (using action-scheduler) to run remote
	 * notifications.
	 */
	private static function update_stored_state_and_possibly_run_remote_notifications() {
		$stored_state = RemoteInboxNotificationsEngine::get_stored_state();
		// If the stored_state is the same, we don't need to run remote notifications to avoid unnecessary action scheduling.
		if ( true === $stored_state->there_are_now_products ) {
			return;
		}

		$stored_state->there_are_now_products = true;
		RemoteInboxNotificationsEngine::update_stored_state( $stored_state );

		// Run self::run_remote_notifications asynchronously.
		as_enqueue_async_action( self::ASYNC_RUN_REMOTE_NOTIFICATIONS_ACTION_NAME );
	}
}
Admin/RemoteInboxNotifications/TransformerService.php000064400000003756152333011040017120 0ustar00<?php

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

use InvalidArgumentException;
use stdClass;

/**
 * A simple service class for the Transformer classes.
 *
 * Class TransformerService
 *
 * @package Automattic\WooCommerce\Admin\RemoteInboxNotifications
 */
class TransformerService {
	/**
	 * Create a transformer object by name.
	 *
	 * @param string $name name of the transformer.
	 *
	 * @return TransformerInterface|null
	 */
	public static function create_transformer( $name ) {
		$camel_cased = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $name ) ) );

		$classname = __NAMESPACE__ . '\\Transformers\\' . $camel_cased;
		if ( ! class_exists( $classname ) ) {
			return null;
		}

		return new $classname();
	}

	/**
	 * Apply transformers to the given value.
	 *
	 * @param mixed  $target_value a value to transform.
	 * @param array  $transformer_configs transform configuration.
	 * @param string $default default value.
	 *
	 * @throws InvalidArgumentException Throws when one of the requried arguments is missing.
	 * @return mixed|null
	 */
	public static function apply( $target_value, array $transformer_configs, $default ) {
		foreach ( $transformer_configs as $transformer_config ) {
			if ( ! isset( $transformer_config->use ) ) {
				throw new InvalidArgumentException( 'Missing required config value: use' );
			}

			if ( ! isset( $transformer_config->arguments ) ) {
				$transformer_config->arguments = null;
			}

			$transformer = self::create_transformer( $transformer_config->use );
			if ( null === $transformer ) {
				throw new InvalidArgumentException( "Unable to find a transformer by name: {$transformer_config->use}" );
			}

			$transformed_value = $transformer->transform( $target_value, $transformer_config->arguments, $default );
			// if the transformer returns null, then return the previously transformed value.
			if ( null === $transformed_value ) {
				return $target_value;
			}

			$target_value = $transformed_value;
		}

		return $target_value;
	}
}
Admin/RemoteInboxNotifications/PassRuleProcessor.php000064400000001407152333011040016722 0ustar00<?php
/**
 * Rule processor that passes. This is required because an empty set of rules
 * (or predicate) evaluates to false.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor that passes.
 */
class PassRuleProcessor implements RuleProcessorInterface {
	/**
	 * Passes the rule.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool Always true.
	 */
	public function process( $rule, $stored_state ) {
		return true;
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		return true;
	}
}
Admin/RemoteInboxNotifications/TotalPaymentsVolumeProcessor.php000064400000003410152333011040021154 0ustar00<?php
/**
 * Rule processor that passes when a store's payments volume exceeds a provided amount.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\API\Reports\Revenue\Query as RevenueQuery;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;

/**
 * Rule processor that passes when a store's payments volume exceeds a provided amount.
 */
class TotalPaymentsVolumeProcessor implements RuleProcessorInterface {
	/**
	 * Compare against the store's total payments volume.
	 *
	 * @param object $rule         The rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the operation.
	 */
	public function process( $rule, $stored_state ) {
		$dates      = TimeInterval::get_timeframe_dates( $rule->timeframe );
		$reports_revenue = new RevenueQuery(
			array(
				'before' => $dates['end'],
				'after'  => $dates['start'],
				'interval' => 'year',
				'fields' => array( 'total_sales' ),
			)
		);
		$report_data    = $reports_revenue->get_data();
		$value          = $report_data->totals->total_sales;

		return ComparisonOperation::compare(
			$value,
			$rule->value,
			$rule->operation
		);
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		$allowed_timeframes = array(
			'last_week',
			'last_month',
			'last_quarter',
			'last_6_months',
			'last_year',
		);

		if ( ! isset( $rule->timeframe ) || ! in_array( $rule->timeframe, $allowed_timeframes, true ) ) {
			return false;
		}

		if ( ! isset( $rule->value ) ) {
			return false;
		}

		if ( ! isset( $rule->operation ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/NoteStatusRuleProcessor.php000064400000002452152333011040020126 0ustar00<?php
/**
 * Rule processor that compares against the status of another note. For
 * example, this could be used to conditionally create a note only if another
 * note has not been actioned.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Notes;

/**
 * Rule processor that compares against the status of another note.
 */
class NoteStatusRuleProcessor implements RuleProcessorInterface {
	/**
	 * Compare against the status of another note.
	 *
	 * @param object $rule         The rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the operation.
	 */
	public function process( $rule, $stored_state ) {
		$status = Notes::get_note_status( $rule->note_name );
		if ( ! $status ) {
			return false;
		}

		return ComparisonOperation::compare(
			$status,
			$rule->status,
			$rule->operation
		);
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->note_name ) ) {
			return false;
		}

		if ( ! isset( $rule->status ) ) {
			return false;
		}

		if ( ! isset( $rule->operation ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/FailRuleProcessor.php000064400000001261152333011040016665 0ustar00<?php
/**
 * Rule processor that fails.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor that fails.
 */
class FailRuleProcessor implements RuleProcessorInterface {
	/**
	 * Fails the rule.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool Always false.
	 */
	public function process( $rule, $stored_state ) {
		return false;
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		return true;
	}
}
Admin/RemoteInboxNotifications/WooCommerceAdminUpdatedRuleProcessor.php000064400000001561152333011040022514 0ustar00<?php
/**
 * Rule processor for sending when WooCommerce Admin has been updated.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor for sending when WooCommerce Admin has been updated.
 */
class WooCommerceAdminUpdatedRuleProcessor implements RuleProcessorInterface {
	/**
	 * Process the rule.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool Whether the rule passes or not.
	 */
	public function process( $rule, $stored_state ) {
		return get_option( RemoteInboxNotificationsEngine::WCA_UPDATED_OPTION_NAME, false );
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		return true;
	}
}
Admin/RemoteInboxNotifications/StoredStateRuleProcessor.php000064400000002346152333011040020260 0ustar00<?php
/**
 * Rule processor that performs a comparison operation against a value in the
 * stored state object.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor that performs a comparison operation against a value in the
 * stored state object.
 */
class StoredStateRuleProcessor implements RuleProcessorInterface {
	/**
	 * Performs a comparison operation against a value in the stored state object.
	 *
	 * @param object $rule         The rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the operation.
	 */
	public function process( $rule, $stored_state ) {
		if ( ! isset( $stored_state->{$rule->index} ) ) {
			return false;
		}

		return ComparisonOperation::compare(
			$stored_state->{$rule->index},
			$rule->value,
			$rule->operation
		);
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->index ) ) {
			return false;
		}

		if ( ! isset( $rule->value ) ) {
			return false;
		}

		if ( ! isset( $rule->operation ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/OrRuleProcessor.php000064400000002613152333011040016374 0ustar00<?php
/**
 * Rule processor that performs an OR operation on the rule's left and right
 * operands.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor that performs an OR operation on the rule's left and right
 * operands.
 */
class OrRuleProcessor implements RuleProcessorInterface {
	/**
	 * Constructor.
	 *
	 * @param RuleEvaluator $rule_evaluator The rule evaluator to use.
	 */
	public function __construct( $rule_evaluator = null ) {
		$this->rule_evaluator = null === $rule_evaluator
			? new RuleEvaluator()
			: $rule_evaluator;
	}

	/**
	 * Performs an OR operation on the rule's left and right operands.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the operation.
	 */
	public function process( $rule, $stored_state ) {
		foreach ( $rule->operands as $operand ) {
			$evaluated_operand = $this->rule_evaluator->evaluate(
				$operand,
				$stored_state
			);

			if ( $evaluated_operand ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->operands ) || ! is_array( $rule->operands ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/BaseLocationCountryRuleProcessor.php000064400000003123152333011040021740 0ustar00<?php
/**
 * Rule processor that performs a comparison operation against the base
 * location - country.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor that performs a comparison operation against the base
 * location - country.
 */
class BaseLocationCountryRuleProcessor implements RuleProcessorInterface {
	/**
	 * Performs a comparison operation against the base location - country.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the operation.
	 */
	public function process( $rule, $stored_state ) {
		$base_location = wc_get_base_location();
		if ( ! $base_location ) {
			return false;
		}

		// Return false if the location is the default country and if onboarding hasn't been finished or the store address not been updated.
		if ( 'US' === $base_location['country'] && 'CA' === $base_location['state'] && empty( get_option( 'woocommerce_store_address', '' ) ) && OnboardingProfile::needs_completion() ) {
			return false;
		}

		return ComparisonOperation::compare(
			$base_location['country'],
			$rule->value,
			$rule->operation
		);
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->value ) ) {
			return false;
		}

		if ( ! isset( $rule->operation ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/IsEcommerceRuleProcessor.php000064400000002161152333011040020205 0ustar00<?php
/**
 * Rule processor that passes (or fails) when the site is on the eCommerce
 * plan.
 *
 * @package WooCommerce\Admin\Classes
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor that passes (or fails) when the site is on the eCommerce
 * plan.
 */
class IsEcommerceRuleProcessor implements RuleProcessorInterface {
	/**
	 * Passes (or fails) based on whether the site is on the eCommerce plan or
	 * not.
	 *
	 * @param object $rule         The rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the operation.
	 */
	public function process( $rule, $stored_state ) {
		if ( ! function_exists( 'wc_calypso_bridge_is_ecommerce_plan' ) ) {
			return false === $rule->value;
		}

		return (bool) wc_calypso_bridge_is_ecommerce_plan() === $rule->value;
	}

	/**
	 * Validate the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->value ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/Transformers/Count.php000064400000001562152333011040017043 0ustar00<?php

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers;

use Automattic\WooCommerce\Admin\RemoteInboxNotifications\TransformerInterface;
use stdClass;

/**
 * Count elements in Array.
 *
 * @package Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers
 */
class Count implements TransformerInterface {
	/**
	 *  Count elements in Array.
	 *
	 * @param array         $value an array to count.
	 * @param stdClass|null $arguments arguments.
	 * @param string|null   $default default value.
	 *
	 * @return number
	 */
	public function transform( $value, stdClass $arguments = null, $default = null ) {
		return count( $value );
	}

	/**
	 * Validate Transformer arguments.
	 *
	 * @param stdClass|null $arguments arguments to validate.
	 *
	 * @return mixed
	 */
	public function validate( stdClass $arguments = null ) {
		return true;
	}
}
Admin/RemoteInboxNotifications/Transformers/ArrayKeys.php000064400000001626152333011040017666 0ustar00<?php

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers;

use Automattic\WooCommerce\Admin\RemoteInboxNotifications\TransformerInterface;
use stdClass;

/**
 * Search array value by one of its key.
 *
 * @package Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers
 */
class ArrayKeys implements TransformerInterface {
	/**
	 * Search array value by one of its key.
	 *
	 * @param mixed         $value a value to transform.
	 * @param stdClass|null $arguments arguments.
	 * @param string|null   $default default value.
	 *
	 * @return mixed
	 */
	public function transform( $value, stdClass $arguments = null, $default = null ) {
		return array_keys( $value );
	}

	/**
	 * Validate Transformer arguments.
	 *
	 * @param stdClass|null $arguments arguments to validate.
	 *
	 * @return mixed
	 */
	public function validate( stdClass $arguments = null ) {
		return true;
	}
}
Admin/RemoteInboxNotifications/Transformers/DotNotation.php000064400000003545152333011040020220 0ustar00<?php

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers;

use Automattic\WooCommerce\Admin\RemoteInboxNotifications\TransformerInterface;
use InvalidArgumentException;
use stdClass;

/**
 * Find an array value by dot notation.
 *
 * @package Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers
 */
class DotNotation implements TransformerInterface {

	/**
	 * Find given path from the given value.
	 *
	 * @param mixed         $value a value to transform.
	 * @param stdClass|null $arguments required argument 'path'.
	 * @param string|null   $default default value.
	 *
	 * @throws InvalidArgumentException Throws when the required 'path' is missing.
	 *
	 * @return mixed
	 */
	public function transform( $value, stdclass $arguments = null, $default = null ) {
		if ( is_object( $value ) ) {
			// if the value is an object, convert it to an array.
			$value = json_decode( wp_json_encode( $value ), true );
		}

		return $this->get( $value, $arguments->path, $default );
	}

	/**
	 * Find the given $path in $array by dot notation.
	 *
	 * @param array  $array an array to search in.
	 * @param string $path a path in the given array.
	 * @param null   $default default value to return if $path was not found.
	 *
	 * @return mixed|null
	 */
	public function get( $array, $path, $default = null ) {
		if ( isset( $array[ $path ] ) ) {
			return $array[ $path ];
		}

		foreach ( explode( '.', $path ) as $segment ) {
			if ( ! is_array( $array ) || ! array_key_exists( $segment, $array ) ) {
				return $default;
			}

			$array = $array[ $segment ];
		}

		return $array;
	}

	/**
	 * Validate Transformer arguments.
	 *
	 * @param stdClass|null $arguments arguments to validate.
	 *
	 * @return mixed
	 */
	public function validate( stdClass $arguments = null ) {
		if ( ! isset( $arguments->path ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/Transformers/ArraySearch.php000064400000002306152333011040020154 0ustar00<?php

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers;

use Automattic\WooCommerce\Admin\RemoteInboxNotifications\TransformerInterface;
use InvalidArgumentException;
use stdClass;

/**
 * Searches a given a given value in the array.
 *
 * @package Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers
 */
class ArraySearch implements TransformerInterface {
	/**
	 * Search a given value in the array.
	 *
	 * @param mixed         $value a value to transform.
	 * @param stdClass|null $arguments required argument 'value'.
	 * @param string|null   $default default value.
	 *
	 * @throws InvalidArgumentException Throws when the required 'value' is missing.
	 *
	 * @return mixed|null
	 */
	public function transform( $value, stdClass $arguments = null, $default = null ) {
		$key = array_search( $arguments->value, $value, true );
		if ( false !== $key ) {
			return $value[ $key ];
		}

		return null;
	}

	/**
	 * Validate Transformer arguments.
	 *
	 * @param stdClass|null $arguments arguments to validate.
	 *
	 * @return mixed
	 */
	public function validate( stdClass $arguments = null ) {
		if ( ! isset( $arguments->value ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/Transformers/ArrayFlatten.php000064400000002000152333011040020333 0ustar00<?php

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers;

use Automattic\WooCommerce\Admin\RemoteInboxNotifications\TransformerInterface;
use stdClass;

/**
 * Flatten nested array.
 *
 * @package Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers
 */
class ArrayFlatten implements TransformerInterface {
	/**
	 * Search a given value in the array.
	 *
	 * @param mixed         $value a value to transform.
	 * @param stdClass|null $arguments arguments.
	 * @param string|null   $default default value.
	 *
	 * @return mixed|null
	 */
	public function transform( $value, stdClass $arguments = null, $default = null ) {
		$return = array();
		array_walk_recursive(
			$value,
			function( $item ) use ( &$return ) {
				$return[] = $item;
			}
		);

		return $return;
	}

	/**
	 * Validate Transformer arguments.
	 *
	 * @param stdClass|null $arguments arguments to validate.
	 *
	 * @return mixed
	 */
	public function validate( stdClass $arguments = null ) {
		return true;
	}
}
Admin/RemoteInboxNotifications/Transformers/ArrayColumn.php000064400000002161152333011040020203 0ustar00<?php

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers;

use Automattic\WooCommerce\Admin\RemoteInboxNotifications\TransformerInterface;
use InvalidArgumentException;
use stdClass;

/**
 * Search array value by one of its key.
 *
 * @package Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers
 */
class ArrayColumn implements TransformerInterface {
	/**
	 * Search array value by one of its key.
	 *
	 * @param mixed         $value a value to transform.
	 * @param stdClass|null $arguments required arguments 'key'.
	 * @param string|null   $default default value.
	 *
	 * @throws InvalidArgumentException Throws when the required argument 'key' is missing.
	 *
	 * @return mixed
	 */
	public function transform( $value, stdClass $arguments = null, $default = null ) {
		return array_column( $value, $arguments->key );
	}

	/**
	 * Validate Transformer arguments.
	 *
	 * @param stdClass|null $arguments arguments to validate.
	 *
	 * @return mixed
	 */
	public function validate( stdClass $arguments = null ) {
		if ( ! isset( $arguments->key ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/Transformers/ArrayValues.php000064400000001632152333011040020207 0ustar00<?php

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers;

use Automattic\WooCommerce\Admin\RemoteInboxNotifications\TransformerInterface;
use stdClass;

/**
 * Search array value by one of its key.
 *
 * @package Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers
 */
class ArrayValues implements TransformerInterface {
	/**
	 * Search array value by one of its key.
	 *
	 * @param mixed         $value a value to transform.
	 * @param stdClass|null $arguments arguments.
	 * @param string|null   $default default value.
	 *
	 * @return mixed
	 */
	public function transform( $value, stdClass $arguments = null, $default = null ) {
		return array_values( $value );
	}

	/**
	 * Validate Transformer arguments.
	 *
	 * @param stdClass|null $arguments arguments to validate.
	 *
	 * @return mixed
	 */
	public function validate( stdClass $arguments = null ) {
		return true;
	}
}
Admin/RemoteInboxNotifications/OptionRuleProcessor.php000064400000004345152333011040017270 0ustar00<?php
/**
 * Rule processor that performs a comparison operation against an option value.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor that performs a comparison operation against an option value.
 */
class OptionRuleProcessor implements RuleProcessorInterface {
	/**
	 * Performs a comparison operation against the option value.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the operation.
	 */
	public function process( $rule, $stored_state ) {
		$is_contains   = $rule->operation && strpos( $rule->operation, 'contains' ) !== false;
		$default_value = $is_contains ? array() : false;
		$default       = isset( $rule->default ) ? $rule->default : $default_value;
		$option_value  = get_option( $rule->option_name, $default );

		if ( $is_contains && ! is_array( $option_value ) ) {
			$logger = wc_get_logger();
			$logger->warning(
				sprintf(
					'ComparisonOperation "%s" option value "%s" is not an array, defaulting to empty array.',
					$rule->operation,
					$rule->option_name
				),
				array(
					'option_value' => $option_value,
					'rule'         => $rule,
				)
			);
			$option_value = array();
		}

		if ( isset( $rule->transformers ) && is_array( $rule->transformers ) ) {
			$option_value = TransformerService::apply( $option_value, $rule->transformers, $default );
		}

		return ComparisonOperation::compare(
			$option_value,
			$rule->value,
			$rule->operation
		);
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->option_name ) ) {
			return false;
		}

		if ( ! isset( $rule->value ) ) {
			return false;
		}

		if ( ! isset( $rule->operation ) ) {
			return false;
		}

		if ( isset( $rule->transformers ) && is_array( $rule->transformers ) ) {
			foreach ( $rule->transformers as $transform_args ) {
				$transformer = TransformerService::create_transformer( $transform_args->use );
				if ( ! $transformer->validate( $transform_args->arguments ) ) {
					return false;
				}
			}
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/DataSourcePoller.php000064400000012316152333011040016475 0ustar00<?php
/**
 * Handles polling and storage of specs
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Specs data source poller class.
 * This handles polling specs from JSON endpoints, and
 * stores the specs in to the database as an option.
 */
class DataSourcePoller extends \Automattic\WooCommerce\Admin\DataSourcePoller {
	const ID           = 'remote_inbox_notifications';
	const DATA_SOURCES = array(
		'https://woocommerce.com/wp-json/wccom/inbox-notifications/1.0/notifications.json',
	);
	/**
	 * Class instance.
	 *
	 * @var Analytics instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self(
				self::ID,
				self::DATA_SOURCES,
				array(
					'spec_key' => 'slug',
				)
			);
		}
		return self::$instance;
	}

	/**
	 * Validate the spec.
	 *
	 * @param object $spec The spec to validate.
	 * @param string $url  The url of the feed that provided the spec.
	 *
	 * @return bool The result of the validation.
	 */
	protected function validate_spec( $spec, $url ) {
		$logger         = self::get_logger();
		$logger_context = array( 'source' => $url );

		if ( ! isset( $spec->slug ) ) {
			$logger->error(
				'Spec is invalid because the slug is missing in feed',
				$logger_context
			);
			// phpcs:ignore
			$logger->error( print_r( $spec, true ), $logger_context );

			return false;
		}

		if ( ! isset( $spec->status ) ) {
			$logger->error(
				'Spec is invalid because the status is missing in feed',
				$logger_context
			);
			// phpcs:ignore
			$logger->error( print_r( $spec, true ), $logger_context );

			return false;
		}

		if ( ! isset( $spec->locales ) || ! is_array( $spec->locales ) ) {
			$logger->error(
				'Spec is invalid because the status is missing or empty in feed',
				$logger_context
			);
			// phpcs:ignore
			$logger->error( print_r( $spec, true ), $logger_context );

			return false;
		}

		if ( null === SpecRunner::get_locale( $spec->locales ) ) {
			$logger->error(
				'Spec is invalid because the locale could not be retrieved in feed',
				$logger_context
			);
			// phpcs:ignore
			$logger->error( print_r( $spec, true ), $logger_context );

			return false;
		}

		if ( ! isset( $spec->type ) ) {
			$logger->error(
				'Spec is invalid because the type is missing in feed',
				$logger_context
			);
			// phpcs:ignore
			$logger->error( print_r( $spec, true ), $logger_context );

			return false;
		}

		if ( isset( $spec->actions ) && is_array( $spec->actions ) ) {
			foreach ( $spec->actions as $action ) {
				if ( ! $this->validate_action( $action, $url ) ) {
					$logger->error(
						'Spec is invalid because an action is invalid in feed',
						$logger_context
					);
					// phpcs:ignore
					$logger->error( print_r( $spec, true ), $logger_context );

					return false;
				}
			}
		}

		if ( isset( $spec->rules ) && is_array( $spec->rules ) ) {
			foreach ( $spec->rules as $rule ) {
				if ( ! isset( $rule->type ) ) {
					$logger->error(
						'Spec is invalid because a rule type is empty in feed',
						$logger_context
					);
					// phpcs:ignore
					$logger->error( print_r( $rule, true ), $logger_context );
					// phpcs:ignore
					$logger->error( print_r( $spec, true ), $logger_context );

					return false;
				}

				$processor = GetRuleProcessor::get_processor( $rule->type );

				if ( ! $processor->validate( $rule ) ) {
					$logger->error(
						'Spec is invalid because a rule is invalid in feed',
						$logger_context
					);
					// phpcs:ignore
					$logger->error( print_r( $rule, true ), $logger_context );
					// phpcs:ignore
					$logger->error( print_r( $spec, true ), $logger_context );

					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Validate the action.
	 *
	 * @param object $action The action to validate.
	 * @param string $url    The url of the feed containing the action (for error reporting).
	 *
	 * @return bool The result of the validation.
	 */
	private function validate_action( $action, $url ) {
		$logger         = self::get_logger();
		$logger_context = array( 'source' => $url );

		if ( ! isset( $action->locales ) || ! is_array( $action->locales ) ) {
			$logger->error(
				'Action is invalid because it has empty or missing locales in feed',
				$logger_context
			);
			// phpcs:ignore
			$logger->error( print_r( $action, true ), $logger_context );

			return false;
		}

		if ( null === SpecRunner::get_action_locale( $action->locales ) ) {
			$logger->error(
				'Action is invalid because the locale could not be retrieved in feed',
				$logger_context
			);
			// phpcs:ignore
			$logger->error( print_r( $action, true ), $logger_context );

			return false;
		}

		if ( ! isset( $action->name ) ) {
			$logger->error(
				'Action is invalid because the name is missing in feed',
				$logger_context
			);
			// phpcs:ignore
			$logger->error( print_r( $action, true ), $logger_context );

			return false;
		}

		if ( ! isset( $action->status ) ) {
			$logger->error(
				'Action is invalid because the status is missing in feed',
				$logger_context
			);
			// phpcs:ignore
			$logger->error( print_r( $action, true ), $logger_context );

			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/OnboardingProfileRuleProcessor.php000064400000002600152333011040021413 0ustar00<?php
/**
 * Rule processor that performs a comparison operation against a value in the
 * onboarding profile.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor that performs a comparison operation against a value in the
 * onboarding profile.
 */
class OnboardingProfileRuleProcessor implements RuleProcessorInterface {
	/**
	 * Performs a comparison operation against a value in the onboarding
	 * profile.
	 *
	 * @param object $rule         The rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the operation.
	 */
	public function process( $rule, $stored_state ) {
		$onboarding_profile = get_option( 'woocommerce_onboarding_profile' );

		if ( empty( $onboarding_profile ) ) {
			return false;
		}

		if ( ! isset( $onboarding_profile[ $rule->index ] ) ) {
			return false;
		}

		return ComparisonOperation::compare(
			$onboarding_profile[ $rule->index ],
			$rule->value,
			$rule->operation
		);
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->index ) ) {
			return false;
		}

		if ( ! isset( $rule->value ) ) {
			return false;
		}

		if ( ! isset( $rule->operation ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/SpecRunner.php000064400000011145152333011040015350 0ustar00<?php
/**
 * Runs a single spec.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\Notes;

/**
 * Runs a single spec.
 */
class SpecRunner {
	/**
	 * Run the spec.
	 *
	 * @param object $spec         The spec to run.
	 * @param object $stored_state Stored state.
	 */
	public static function run_spec( $spec, $stored_state ) {
		$data_store = Notes::load_data_store();

		// Create or update the note.
		$existing_note_ids = $data_store->get_notes_with_name( $spec->slug );
		if ( count( $existing_note_ids ) === 0 ) {
			$note = new Note();
			$note->set_status( Note::E_WC_ADMIN_NOTE_PENDING );
		} else {
			$note = Notes::get_note( $existing_note_ids[0] );
			if ( $note === false ) {
				return;
			}
		}

		// Evaluate the spec and get the new note status.
		$previous_status = $note->get_status();
		$status          = EvaluateAndGetStatus::evaluate(
			$spec,
			$previous_status,
			$stored_state,
			new RuleEvaluator()
		);

		// If the status is changing, update the created date to now.
		if ( $previous_status !== $status ) {
			$note->set_date_created( time() );
		}

		// Get the matching locale or fall back to en-US.
		$locale = self::get_locale( $spec->locales );

		if ( $locale === null ) {
			return;
		}

		// Set up the note.
		$note->set_title( $locale->title );
		$note->set_content( $locale->content );
		$note->set_content_data( isset( $spec->content_data ) ? $spec->content_data : (object) array() );
		$note->set_status( $status );
		$note->set_type( $spec->type );
		$note->set_name( $spec->slug );
		if ( isset( $spec->source ) ) {
			$note->set_source( $spec->source );
		}

		// Recreate actions.
		$note->set_actions( self::get_actions( $spec ) );

		$note->save();
	}

	/**
	 * Get the URL for an action.
	 *
	 * @param object $action The action.
	 *
	 * @return string The URL for the action.
	 */
	private static function get_url( $action ) {
		if ( ! isset( $action->url ) ) {
			return '';
		}

		if ( isset( $action->url_is_admin_query ) && $action->url_is_admin_query ) {
			if ( strpos( $action->url, '&path' ) === 0 ) {
				return wc_admin_url( $action->url );
			}
			return admin_url( $action->url );
		}

		return $action->url;
	}

	/**
	 * Get the locale for the WordPress locale, or fall back to the en_US
	 * locale.
	 *
	 * @param Array $locales The locales to search through.
	 *
	 * @returns object The locale that was found, or null if no matching locale was found.
	 */
	public static function get_locale( $locales ) {
		$wp_locale           = get_user_locale();
		$matching_wp_locales = array_values(
			array_filter(
				$locales,
				function( $l ) use ( $wp_locale ) {
					return $wp_locale === $l->locale;
				}
			)
		);

		if ( count( $matching_wp_locales ) !== 0 ) {
			return $matching_wp_locales[0];
		}

		// Fall back to en_US locale.
		$en_us_locales = array_values(
			array_filter(
				$locales,
				function( $l ) {
					return $l->locale === 'en_US';
				}
			)
		);

		if ( count( $en_us_locales ) !== 0 ) {
			return $en_us_locales[0];
		}

		return null;
	}

	/**
	 * Get the action locale that matches the note locale, or fall back to the
	 * en_US locale.
	 *
	 * @param Array $action_locales The locales from the spec's action.
	 *
	 * @return object The matching locale, or the en_US fallback locale, or null if neither was found.
	 */
	public static function get_action_locale( $action_locales ) {
		$wp_locale           = get_user_locale();
		$matching_wp_locales = array_values(
			array_filter(
				$action_locales,
				function ( $l ) use ( $wp_locale ) {
					return $wp_locale === $l->locale;
				}
			)
		);

		if ( count( $matching_wp_locales ) !== 0 ) {
			return $matching_wp_locales[0];
		}

		// Fall back to en_US locale.
		$en_us_locales = array_values(
			array_filter(
				$action_locales,
				function( $l ) {
					return $l->locale === 'en_US';
				}
			)
		);

		if ( count( $en_us_locales ) !== 0 ) {
			return $en_us_locales[0];
		}

		return null;
	}

	/**
	 * Get the actions for a note.
	 *
	 * @param object $spec The spec.
	 *
	 * @return array The actions.
	 */
	public static function get_actions( $spec ) {
		$note    = new Note();
		$actions = isset( $spec->actions ) ? $spec->actions : array();
		foreach ( $actions as $action ) {
			$action_locale = self::get_action_locale( $action->locales );

			$url = self::get_url( $action );

			$note->add_action(
				$action->name,
				( $action_locale === null || ! isset( $action_locale->label ) )
					? ''
					: $action_locale->label,
				$url,
				$action->status
			);
		}
		return $note->get_actions();
	}
}
Admin/RemoteInboxNotifications/NotRuleProcessor.php000064400000002334152333011040016554 0ustar00<?php
/**
 * Rule processor that negates the rules in the rule's operand.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Rule processor that negates the rules in the rule's operand.
 */
class NotRuleProcessor implements RuleProcessorInterface {
	/**
	 * Constructor.
	 *
	 * @param RuleEvaluator $rule_evaluator The rule evaluator to use.
	 */
	public function __construct( $rule_evaluator = null ) {
		$this->rule_evaluator = null === $rule_evaluator
			? new RuleEvaluator()
			: $rule_evaluator;
	}

	/**
	 * Evaluates the rules in the operand and negates the result.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool The result of the operation.
	 */
	public function process( $rule, $stored_state ) {
		$evaluated_operand = $this->rule_evaluator->evaluate(
			$rule->operand,
			$stored_state
		);

		return ! $evaluated_operand;
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->operand ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/ComparisonOperation.php000064400000002205152333011040017254 0ustar00<?php
/**
 * Compare two operands using the specified operation.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Compare two operands using the specified operation.
 */
class ComparisonOperation {
	/**
	 * Compare two operands using the specified operation.
	 *
	 * @param object $left_operand  The left hand operand.
	 * @param object $right_operand The right hand operand.
	 * @param string $operation     The operation used to compare the operands.
	 */
	public static function compare( $left_operand, $right_operand, $operation ) {
		switch ( $operation ) {
			case '=':
				return $left_operand === $right_operand;
			case '<':
				return $left_operand < $right_operand;
			case '<=':
				return $left_operand <= $right_operand;
			case '>':
				return $left_operand > $right_operand;
			case '>=':
				return $left_operand >= $right_operand;
			case '!=':
				return $left_operand !== $right_operand;
			case 'contains':
				return in_array( $right_operand, $left_operand, true );
			case '!contains':
				return ! in_array( $right_operand, $left_operand, true );
		}

		return false;
	}
}
Admin/RemoteInboxNotifications/GetRuleProcessor.php000064400000003745152333011040016542 0ustar00<?php
/**
 * Gets the processor for the specified rule type.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Class encapsulating getting the processor for a given rule type.
 */
class GetRuleProcessor {
	/**
	 * Get the processor for the specified rule type.
	 *
	 * @param string $rule_type The rule type.
	 *
	 * @return RuleProcessorInterface The matching processor for the specified rule type, or a FailRuleProcessor if no matching processor is found.
	 */
	public static function get_processor( $rule_type ) {
		switch ( $rule_type ) {
			case 'plugins_activated':
				return new PluginsActivatedRuleProcessor();
			case 'publish_after_time':
				return new PublishAfterTimeRuleProcessor();
			case 'publish_before_time':
				return new PublishBeforeTimeRuleProcessor();
			case 'not':
				return new NotRuleProcessor();
			case 'or':
				return new OrRuleProcessor();
			case 'fail':
				return new FailRuleProcessor();
			case 'pass':
				return new PassRuleProcessor();
			case 'plugin_version':
				return new PluginVersionRuleProcessor();
			case 'stored_state':
				return new StoredStateRuleProcessor();
			case 'order_count':
				return new OrderCountRuleProcessor();
			case 'wcadmin_active_for':
				return new WCAdminActiveForRuleProcessor();
			case 'product_count':
				return new ProductCountRuleProcessor();
			case 'onboarding_profile':
				return new OnboardingProfileRuleProcessor();
			case 'is_ecommerce':
				return new IsEcommerceRuleProcessor();
			case 'base_location_country':
				return new BaseLocationCountryRuleProcessor();
			case 'base_location_state':
				return new BaseLocationStateRuleProcessor();
			case 'note_status':
				return new NoteStatusRuleProcessor();
			case 'option':
				return new OptionRuleProcessor();
			case 'wca_updated':
				return new WooCommerceAdminUpdatedRuleProcessor();
			case 'total_payments_value':
				return new TotalPaymentsVolumeProcessor();
		}

		return new FailRuleProcessor();
	}
}
Admin/RemoteInboxNotifications/WCAdminActiveForProvider.php000064400000000767152333011040020074 0ustar00<?php
/**
 * WCAdmin active for provider.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

use Automattic\WooCommerce\Admin\WCAdminHelper;

defined( 'ABSPATH' ) || exit;

/**
 * WCAdminActiveForProvider class
 */
class WCAdminActiveForProvider {
	/**
	 * Get the number of seconds that the store has been active.
	 *
	 * @return number Number of seconds.
	 */
	public function get_wcadmin_active_for_in_seconds() {
		return WCAdminHelper::get_wcadmin_active_for_in_seconds();
	}
}
Admin/RemoteInboxNotifications/PluginsActivatedRuleProcessor.php000064400000002760152333011040021265 0ustar00<?php
/**
 * Rule processor for sending when the provided plugins are activated.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\PluginsProvider\PluginsProvider;

/**
 * Rule processor for sending when the provided plugins are activated.
 */
class PluginsActivatedRuleProcessor implements RuleProcessorInterface {
	/**
	 * Constructor.
	 *
	 * @param PluginsProviderInterface $plugins_provider The plugins provider.
	 */
	public function __construct( $plugins_provider = null ) {
		$this->plugins_provider = null === $plugins_provider
			? new PluginsProvider()
			: $plugins_provider;
	}

	/**
	 * Process the rule.
	 *
	 * @param object $rule         The specific rule being processed by this rule processor.
	 * @param object $stored_state Stored state.
	 *
	 * @return bool Whether the rule passes or not.
	 */
	public function process( $rule, $stored_state ) {
		if ( 0 === count( $rule->plugins ) ) {
			return false;
		}

		$active_plugin_slugs = $this->plugins_provider->get_active_plugin_slugs();

		foreach ( $rule->plugins as $plugin_slug ) {
			if ( ! in_array( $plugin_slug, $active_plugin_slugs, true ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Validates the rule.
	 *
	 * @param object $rule The rule to validate.
	 *
	 * @return bool Pass/fail.
	 */
	public function validate( $rule ) {
		if ( ! isset( $rule->plugins ) || ! is_array( $rule->plugins ) ) {
			return false;
		}

		return true;
	}
}
Admin/RemoteInboxNotifications/EvaluateAndGetStatus.php000064400000003027152333011040017321 0ustar00<?php
/**
 * Evaluates the spec and returns a status.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;

/**
 * Evaluates the spec and returns a status.
 */
class EvaluateAndGetStatus {
	/**
	 * Evaluates the spec and returns a status.
	 *
	 * @param array  $spec The spec to evaluate.
	 * @param string $current_status The note's current status.
	 * @param object $stored_state Stored state.
	 * @param object $rule_evaluator Evaluates rules into true/false.
	 *
	 * @return string The evaluated status.
	 */
	public static function evaluate( $spec, $current_status, $stored_state, $rule_evaluator ) {
		// No rules should leave the note alone.
		if ( ! isset( $spec->rules ) ) {
			return $current_status;
		}

		$evaluated_result = $rule_evaluator->evaluate(
			$spec->rules,
			$stored_state,
			array(
				'slug'   => $spec->slug,
				'source' => 'remote-inbox-notifications',
			)
		);

		// Pending notes should be the spec status if the spec passes,
		// left alone otherwise.
		if ( Note::E_WC_ADMIN_NOTE_PENDING === $current_status ) {
			return $evaluated_result
				? $spec->status
				: Note::E_WC_ADMIN_NOTE_PENDING;
		}

		// When allow_redisplay isn't set, just leave the note alone.
		if ( ! isset( $spec->allow_redisplay ) || ! $spec->allow_redisplay ) {
			return $current_status;
		}

		// allow_redisplay is set, unaction the note if eval to true.
		return $evaluated_result
			? Note::E_WC_ADMIN_NOTE_UNACTIONED
			: $current_status;
	}
}
Admin/RemoteInboxNotifications/OrdersProvider.php000064400000001277152333011040016242 0ustar00<?php
/**
 * Provider for order-related queries and operations.
 */

namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;

defined( 'ABSPATH' ) || exit;

/**
 * Provider for order-related queries and operations.
 */
class OrdersProvider {
	/**
	 * Allowed order statuses for calculating milestones.
	 *
	 * @var array
	 */
	protected $allowed_statuses = array(
		'pending',
		'processing',
		'completed',
	);

	/**
	 * Returns the number of orders.
	 *
	 * @return integer The number of orders.
	 */
	public function get_order_count() {
		$status_counts = array_map( 'wc_orders_count', $this->allowed_statuses );
		$orders_count  = array_sum( $status_counts );

		return $orders_count;
	}
}
Admin/Loader.php000064400000005304152333011040007505 0ustar00<?php

namespace Automattic\WooCommerce\Admin;

use Automattic\WooCommerce\Admin\DeprecatedClassFacade;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Loader Class.
 *
 * @deprecated since 6.3.0, use WooCommerce\Internal\Admin\Loader.
 */
class Loader extends DeprecatedClassFacade {

	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Loader';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '6.3.0';

	/**
	 * Returns if a specific wc-admin feature is enabled.
	 *
	 * @param  string $feature Feature slug.
	 * @return bool Returns true if the feature is enabled.
	 *
	 * @deprecated since 5.0.0, use Features::is_enabled( $feature )
	 */
	public static function is_feature_enabled( $feature ) {
		wc_deprecated_function( 'is_feature_enabled', '5.0', '\Automattic\WooCommerce\Internal\Admin\Features\Features::is_enabled()' );
		return Features::is_enabled( $feature );
	}

	/**
	 * Returns true if we are on a JS powered admin page or
	 * a "classic" (non JS app) powered admin page (an embedded page).
	 *
	 * @deprecated 6.3.0
	 */
	public static function is_admin_or_embed_page() {
		wc_deprecated_function( 'is_admin_or_embed_page', '6.3', '\Automattic\WooCommerce\Admin\PageController::is_admin_or_embed_page()' );
		return PageController::is_admin_or_embed_page();
	}

	/**
	 * Returns true if we are on a JS powered admin page.
	 *
	 * @deprecated 6.3.0
	 */
	public static function is_admin_page() {
		wc_deprecated_function( 'is_admin_page', '6.3', '\Automattic\WooCommerce\Admin\PageController::is_admin_page()' );
		return PageController::is_admin_page();
	}

	/**
	 * Returns true if we are on a "classic" (non JS app) powered admin page.
	 *
	 * @deprecated 6.3.0
	 */
	public static function is_embed_page() {
		wc_deprecated_function( 'is_embed_page', '6.3', '\Automattic\WooCommerce\Admin\PageController::is_embed_page()' );
		return PageController::is_embed_page();
	}

	/**
	 * Determines if a minified JS file should be served.
	 *
	 * @param  boolean $script_debug Only serve unminified files if script debug is on.
	 * @return boolean If js asset should use minified version.
	 *
	 * @deprecated since 6.3.0, use WCAdminAssets::should_use_minified_js_file( $script_debug )
	 */
	public static function should_use_minified_js_file( $script_debug ) {
		// Bail if WC isn't initialized (This can be called from WCAdmin's entrypoint).
		if ( ! defined( 'WC_ABSPATH' ) ) {
			return;
		}
		return WCAdminAssets::should_use_minified_js_file( $script_debug );
	}
}
Admin/Schedulers/SchedulerTraits.php000064400000023360152333011040013507 0ustar00<?php
/**
 * Traits for scheduling actions and dependencies.
 */

namespace Automattic\WooCommerce\Admin\Schedulers;

defined( 'ABSPATH' ) || exit;

/**
 * SchedulerTraits class.
 */
trait SchedulerTraits {
	/**
	 * Action scheduler group.
	 *
	 * @var string|null
	 */
	public static $group = 'wc-admin-data';

	/**
	 * Queue instance.
	 *
	 * @var WC_Queue_Interface
	 */
	protected static $queue = null;

	/**
	 * Add all actions as hooks.
	 */
	public static function init() {
		foreach ( self::get_actions() as $action_name => $action_hook ) {
			$method = new \ReflectionMethod( static::class, $action_name );
			add_action( $action_hook, array( static::class, 'do_action_or_reschedule' ), 10, $method->getNumberOfParameters() );
		}
	}

	/**
	 * Get queue instance.
	 *
	 * @return WC_Queue_Interface
	 */
	public static function queue() {
		if ( is_null( self::$queue ) ) {
			self::$queue = WC()->queue();
		}

		return self::$queue;
	}

	/**
	 * Set queue instance.
	 *
	 * @param WC_Queue_Interface $queue Queue instance.
	 */
	public static function set_queue( $queue ) {
		self::$queue = $queue;
	}

	/**
	 * Gets the default scheduler actions for batching and scheduling actions.
	 */
	public static function get_default_scheduler_actions() {
		return array(
			'schedule_action' => 'wc-admin_schedule_action_' . static::$name,
			'queue_batches'   => 'wc-admin_queue_batches_' . static::$name,
		);
	}

	/**
	 * Gets the actions for this specific scheduler.
	 *
	 * @return array
	 */
	public static function get_scheduler_actions() {
		return array();
	}

	/**
	 * Get all available scheduling actions.
	 * Used to determine action hook names and clear events.
	 */
	public static function get_actions() {
		return array_merge(
			static::get_default_scheduler_actions(),
			static::get_scheduler_actions()
		);
	}

	/**
	 * Get an action tag name from the action name.
	 *
	 * @param string $action_name The action name.
	 * @return string|null
	 */
	public static function get_action( $action_name ) {
		$actions = static::get_actions();
		return isset( $actions[ $action_name ] ) ? $actions[ $action_name ] : null;
	}

	/**
	 * Returns an array of actions and dependencies as key => value pairs.
	 *
	 * @return array
	 */
	public static function get_dependencies() {
		return array();
	}

	/**
	 * Get dependencies associated with an action.
	 *
	 * @param string $action_name The action slug.
	 * @return string|null
	 */
	public static function get_dependency( $action_name ) {
		$dependencies = static::get_dependencies();
		return isset( $dependencies[ $action_name ] ) ? $dependencies[ $action_name ] : null;
	}

	/**
	 * Batch action size.
	 */
	public static function get_batch_sizes() {
		return array(
			'queue_batches' => 100,
		);
	}

	/**
	 * Returns the batch size for an action.
	 *
	 * @param string $action Single batch action name.
	 * @return int Batch size.
	 */
	public static function get_batch_size( $action ) {
		$batch_sizes = static::get_batch_sizes();
		$batch_size  = isset( $batch_sizes[ $action ] ) ? $batch_sizes[ $action ] : 25;

		/**
		 * Filter the batch size for regenerating a report table.
		 *
		 * @param int    $batch_size Batch size.
		 * @param string $action Batch action name.
		 */
		return apply_filters( 'woocommerce_analytics_regenerate_batch_size', $batch_size, static::$name, $action );
	}

	/**
	 * Flatten multidimensional arrays to store for scheduling.
	 *
	 * @param array $args Argument array.
	 * @return string
	 */
	public static function flatten_args( $args ) {
		$flattened = array();

		foreach ( $args as $arg ) {
			if ( is_array( $arg ) ) {
				$flattened[] = self::flatten_args( $arg );
			} else {
				$flattened[] = $arg;
			}
		}

		$string = '[' . implode( ',', $flattened ) . ']';
		return $string;
	}

	/**
	 * Check if existing jobs exist for an action and arguments.
	 *
	 * @param string $action_name Action name.
	 * @param array  $args Array of arguments to pass to action.
	 * @return bool
	 */
	public static function has_existing_jobs( $action_name, $args ) {
		$existing_jobs = self::queue()->search(
			array(
				'status'   => 'pending',
				'per_page' => 1,
				'claimed'  => false,
				'hook'     => static::get_action( $action_name ),
				'search'   => self::flatten_args( $args ),
				'group'    => self::$group,
			)
		);

		if ( $existing_jobs ) {
			$existing_job = current( $existing_jobs );

			// Bail out if there's a pending single action, or a pending scheduled actions.
			if (
				( static::get_action( $action_name ) === $existing_job->get_hook() ) ||
				(
					static::get_action( 'schedule_action' ) === $existing_job->get_hook() &&
					in_array( self::get_action( $action_name ), $existing_job->get_args(), true )
				)
			) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Get the next blocking job for an action.
	 *
	 * @param string $action_name Action name.
	 * @return false|ActionScheduler_Action
	 */
	public static function get_next_blocking_job( $action_name ) {
		$dependency = self::get_dependency( $action_name );

		if ( ! $dependency ) {
			return false;
		}

		$blocking_jobs = self::queue()->search(
			array(
				'status'   => 'pending',
				'orderby'  => 'date',
				'order'    => 'DESC',
				'per_page' => 1,
				'search'   => $dependency, // search is used instead of hook to find queued batch creation.
				'group'    => static::$group,
			)
		);

		$next_job_schedule = null;

		if ( is_array( $blocking_jobs ) ) {
			foreach ( $blocking_jobs as $blocking_job ) {
				$next_job_schedule = self::get_next_action_time( $blocking_job );

				// Ensure that the next schedule is a DateTime (it can be null).
				if ( is_a( $next_job_schedule, 'DateTime' ) ) {
					return $blocking_job;
				}
			}
		}

		return false;
	}

	/**
	 * Check for blocking jobs and reschedule if any exist.
	 */
	public static function do_action_or_reschedule() {
		$action_hook = current_action();
		$action_name = array_search( $action_hook, static::get_actions(), true );
		$args        = func_get_args();

		// Check if any blocking jobs exist and schedule after they've completed
		// or schedule to run now if no blocking jobs exist.
		$blocking_job = static::get_next_blocking_job( $action_name );
		if ( $blocking_job ) {
			$after = new \DateTime();
			self::queue()->schedule_single(
				self::get_next_action_time( $blocking_job )->getTimestamp() + 5,
				$action_hook,
				$args,
				static::$group
			);
		} else {
			call_user_func_array( array( static::class, $action_name ), $args );
		}
	}

	/**
	 * Get the DateTime for the next scheduled time an action should run.
	 * This function allows backwards compatibility with Action Scheduler < v3.0.
	 *
	 * @param \ActionScheduler_Action $action Action.
	 * @return DateTime|null
	 */
	public static function get_next_action_time( $action ) {
		if ( method_exists( $action->get_schedule(), 'get_next' ) ) {
			$after             = new \DateTime();
			$next_job_schedule = $action->get_schedule()->get_next( $after );
		} else {
			$next_job_schedule = $action->get_schedule()->next();
		}

		return $next_job_schedule;
	}

	/**
	 * Schedule an action to run and check for dependencies.
	 *
	 * @param string $action_name Action name.
	 * @param array  $args Array of arguments to pass to action.
	 */
	public static function schedule_action( $action_name, $args = array() ) {
		// Check for existing jobs and bail if they already exist.
		if ( static::has_existing_jobs( $action_name, $args ) ) {
			return;
		}

		$action_hook = static::get_action( $action_name );
		if ( ! $action_hook ) {
			return;
		}

		if (
			// Skip scheduling if Action Scheduler tables have not been initialized.
			! get_option( 'schema-ActionScheduler_StoreSchema' ) ||
			apply_filters( 'woocommerce_analytics_disable_action_scheduling', false )
		) {
			call_user_func_array( array( static::class, $action_name ), $args );
			return;
		}

		self::queue()->schedule_single( time() + 5, $action_hook, $args, static::$group );
	}

	/**
	 * Queue a large number of batch jobs, respecting the batch size limit.
	 * Reduces a range of batches down to "single batch" jobs.
	 *
	 * @param int    $range_start Starting batch number.
	 * @param int    $range_end Ending batch number.
	 * @param string $single_batch_action Action to schedule for a single batch.
	 * @param array  $action_args Action arguments.
	 * @return void
	 */
	public static function queue_batches( $range_start, $range_end, $single_batch_action, $action_args = array() ) {
		$batch_size       = static::get_batch_size( 'queue_batches' );
		$range_size       = 1 + ( $range_end - $range_start );
		$action_timestamp = time() + 5;

		if ( $range_size > $batch_size ) {
			// If the current batch range is larger than a single batch,
			// split the range into $queue_batch_size chunks.
			$chunk_size = (int) ceil( $range_size / $batch_size );

			for ( $i = 0; $i < $batch_size; $i++ ) {
				$batch_start = (int) ( $range_start + ( $i * $chunk_size ) );
				$batch_end   = (int) min( $range_end, $range_start + ( $chunk_size * ( $i + 1 ) ) - 1 );

				if ( $batch_start > $range_end ) {
					return;
				}

				self::schedule_action(
					'queue_batches',
					array( $batch_start, $batch_end, $single_batch_action, $action_args )
				);
			}
		} else {
			// Otherwise, queue the single batches.
			for ( $i = $range_start; $i <= $range_end; $i++ ) {
				$batch_action_args = array_merge( array( $i ), $action_args );
				self::schedule_action( $single_batch_action, $batch_action_args );
			}
		}
	}

	/**
	 * Clears all queued actions.
	 */
	public static function clear_queued_actions() {
		if ( version_compare( \ActionScheduler_Versions::instance()->latest_version(), '3.0', '>=' ) ) {
			\ActionScheduler::store()->cancel_actions_by_group( static::$group );
		} else {
			$actions = static::get_actions();
			foreach ( $actions as $action ) {
				self::queue()->cancel_all( $action, null, static::$group );
			}
		}
	}
}
Admin/PluginsInstaller.php000064400000006564152333011040011607 0ustar00<?php
/**
 * PluginsInstaller
 *
 * Installer to allow plugin installation via URL query.
 */

namespace Automattic\WooCommerce\Admin;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\API\Plugins;
use Automattic\WooCommerce\Admin\Features\TransientNotices;

/**
 * Class PluginsInstaller
 */
class PluginsInstaller {

	/**
	 * Constructor
	 */
	public static function init() {
		add_action( 'admin_init', array( __CLASS__, 'possibly_install_activate_plugins' ) );
	}

	/**
	 * Check if an install or activation is being requested via URL query.
	 */
	public static function possibly_install_activate_plugins() {
		/* phpcs:disable WordPress.Security.NonceVerification.Recommended */
		if (
			! isset( $_GET['plugin_action'] ) ||
			! isset( $_GET['plugins'] ) ||
			! current_user_can( 'install_plugins' ) ||
			! isset( $_GET['nonce'] )
		) {
			return;
		}

		$nonce = sanitize_text_field( wp_unslash( $_GET['nonce'] ) );

		if ( ! wp_verify_nonce( $nonce, 'install-plugin' ) ) {
			wp_nonce_ays( 'install-plugin' );
		}

		$plugins       = sanitize_text_field( wp_unslash( $_GET['plugins'] ) );
		$plugin_action = sanitize_text_field( wp_unslash( $_GET['plugin_action'] ) );
		/* phpcs:enable WordPress.Security.NonceVerification.Recommended */

		$plugins_api     = new Plugins();
		$install_result  = null;
		$activate_result = null;

		switch ( $plugin_action ) {
			case 'install':
				$install_result = $plugins_api->install_plugins( array( 'plugins' => $plugins ) );
				break;
			case 'activate':
				$activate_result = $plugins_api->activate_plugins( array( 'plugins' => $plugins ) );
				break;
			case 'install-activate':
				$install_result  = $plugins_api->install_plugins( array( 'plugins' => $plugins ) );
				$activate_result = $plugins_api->activate_plugins( array( 'plugins' => implode( ',', $install_result['data']['installed'] ) ) );
				break;
		}

		self::cache_results( $plugins, $install_result, $activate_result );
		self::redirect_to_referer();
	}

	/**
	 * Display the results of installation and activation on the page.
	 *
	 * @param string $plugins Comma separated list of plugins.
	 * @param array  $install_result Result of installation.
	 * @param array  $activate_result Result of activation.
	 */
	public static function cache_results( $plugins, $install_result, $activate_result ) {
		if ( ! $install_result && ! $activate_result ) {
			return;
		}

		if ( is_wp_error( $install_result ) || is_wp_error( $activate_result ) ) {
			$message = $activate_result ? $activate_result->get_error_message() : $install_result->get_error_message();
		} else {
			$message = $activate_result ? $activate_result['message'] : $install_result['message'];
		}

		TransientNotices::add(
			array(
				'user_id' => get_current_user_id(),
				'id'      => 'plugin-installer-' . str_replace( ',', '-', $plugins ),
				'status'  => 'success',
				'content' => $message,
			)
		);
	}

	/**
	 * Redirect back to the referring page if one exists.
	 */
	public static function redirect_to_referer() {
		$referer = wp_get_referer();
		if ( $referer && 0 !== strpos( $referer, wp_login_url() ) ) {
			wp_safe_redirect( $referer );
			exit();
		}

		if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
			return;
		}

		$url = remove_query_arg( 'plugin_action', wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore sanitization ok.
		$url = remove_query_arg( 'plugins', $url );
		wp_safe_redirect( $url );
		exit();
	}
}
Admin/Features/PaymentGatewaySuggestions/Init.php000064400000005517152333011040016160 0ustar00<?php
/**
 * Handles running payment gateway suggestion specs
 */

namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\DefaultPaymentGateways;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\PaymentGatewaysController;

/**
 * Remote Payment Methods engine.
 * This goes through the specs and gets eligible payment gateways.
 */
class Init {
	/**
	 * Option name for dismissed payment method suggestions.
	 */
	const RECOMMENDED_PAYMENT_PLUGINS_DISMISS_OPTION = 'woocommerce_setting_payments_recommendations_hidden';

	/**
	 * Constructor.
	 */
	public function __construct() {
		PaymentGatewaysController::init();
	}

	/**
	 * Go through the specs and run them.
	 *
	 * @param array|null $specs payment suggestion spec array.
	 * @return array
	 */
	public static function get_suggestions( array $specs = null ) {
		$suggestions = array();
		if ( null === $specs ) {
			$specs = self::get_specs();
		}

		foreach ( $specs as $spec ) {
			$suggestion    = EvaluateSuggestion::evaluate( $spec );
			$suggestions[] = $suggestion;
		}

		return array_values(
			array_filter(
				$suggestions,
				function( $suggestion ) {
					return ! property_exists( $suggestion, 'is_visible' ) || $suggestion->is_visible;
				}
			)
		);

	}

	/**
	 * Delete the specs transient.
	 */
	public static function delete_specs_transient() {
		PaymentGatewaySuggestionsDataSourcePoller::get_instance()->delete_specs_transient();
	}

	/**
	 * Get specs or fetch remotely if they don't exist.
	 */
	public static function get_specs() {
		if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
			return apply_filters( 'woocommerce_admin_payment_gateway_suggestion_specs', DefaultPaymentGateways::get_all() );
		}
		$specs = PaymentGatewaySuggestionsDataSourcePoller::get_instance()->get_specs_from_data_sources();

		// Fetch specs if they don't yet exist.
		if ( false === $specs || ! is_array( $specs ) || 0 === count( $specs ) ) {
			return apply_filters( 'woocommerce_admin_payment_gateway_suggestion_specs', DefaultPaymentGateways::get_all() );
		}

		return apply_filters( 'woocommerce_admin_payment_gateway_suggestion_specs', $specs );
	}

	/**
	 * Check if suggestions should be shown in the settings screen.
	 *
	 * @return bool
	 */
	public static function should_display() {
		if ( 'yes' === get_option( self::RECOMMENDED_PAYMENT_PLUGINS_DISMISS_OPTION, 'no' ) ) {
			return false;
		}

		if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
			return false;
		}

		return apply_filters( 'woocommerce_allow_payment_recommendations', true );
	}

	/**
	 * Dismiss the suggestions.
	 */
	public static function dismiss() {
		return update_option( self::RECOMMENDED_PAYMENT_PLUGINS_DISMISS_OPTION, 'yes' );
	}
}
Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php000064400000010671152333011040022440 0ustar00<?php
/**
 * Logic for extending WC_REST_Payment_Gateways_Controller.
 */

namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions;

use Automattic\WooCommerce\Admin\Features\TransientNotices;

defined( 'ABSPATH' ) || exit;

/**
 * PaymentGateway class
 */
class PaymentGatewaysController {

	/**
	 * Initialize payment gateway changes.
	 */
	public static function init() {
		add_filter( 'woocommerce_rest_prepare_payment_gateway', array( __CLASS__, 'extend_response' ), 10, 3 );
		add_filter( 'admin_init', array( __CLASS__, 'possibly_do_connection_return_action' ) );
		add_action( 'woocommerce_admin_payment_gateway_connection_return', array( __CLASS__, 'handle_successfull_connection' ) );
	}

	/**
	 * Add necessary fields to REST API response.
	 *
	 * @param  WP_REST_Response   $response   Response data.
	 * @param  WC_Payment_Gateway $gateway    Payment gateway object.
	 * @param  WP_REST_Request    $request    Request object.
	 * @return WP_REST_Response
	 */
	public static function extend_response( $response, $gateway, $request ) {
		$data = $response->get_data();

		$data['needs_setup']          = $gateway->needs_setup();
		$data['post_install_scripts'] = self::get_post_install_scripts( $gateway );
		$data['settings_url']         = method_exists( $gateway, 'get_settings_url' )
			? $gateway->get_settings_url()
			: admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . strtolower( $gateway->id ) );

		$return_url             = wc_admin_url( '&task=payments&connection-return=' . strtolower( $gateway->id ) . '&_wpnonce=' . wp_create_nonce( 'connection-return' ) );
		$data['connection_url'] = method_exists( $gateway, 'get_connection_url' )
			? $gateway->get_connection_url( $return_url )
			: null;

		$data['setup_help_text'] = method_exists( $gateway, 'get_setup_help_text' )
			? $gateway->get_setup_help_text()
			: null;

		$data['required_settings_keys'] = method_exists( $gateway, 'get_required_settings_keys' )
			? $gateway->get_required_settings_keys()
			: array();

		$response->set_data( $data );

		return $response;
	}

	/**
	 * Get payment gateway scripts for post-install.
	 *
	 * @param  WC_Payment_Gateway $gateway Payment gateway object.
	 * @return array Install scripts.
	 */
	public static function get_post_install_scripts( $gateway ) {
		$scripts    = array();
		$wp_scripts = wp_scripts();

		$handles = method_exists( $gateway, 'get_post_install_script_handles' )
			? $gateway->get_post_install_script_handles()
			: array();

		foreach ( $handles as $handle ) {
			if ( isset( $wp_scripts->registered[ $handle ] ) ) {
				$scripts[] = $wp_scripts->registered[ $handle ];
			}
		}

		return $scripts;
	}

	/**
	 * Call an action after a gating has been successfully returned.
	 */
	public static function possibly_do_connection_return_action() {
		if (
			! isset( $_GET['page'] ) ||
			'wc-admin' !== $_GET['page'] ||
			! isset( $_GET['task'] ) ||
			'payments' !== $_GET['task'] ||
			! isset( $_GET['connection-return'] ) ||
			! isset( $_GET['_wpnonce'] ) ||
			! wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wpnonce'] ) ), 'connection-return' )
		) {
			return;
		}

		$gateway_id = sanitize_text_field( wp_unslash( $_GET['connection-return'] ) );

		do_action( 'woocommerce_admin_payment_gateway_connection_return', $gateway_id );
	}

	/**
	 * Handle a successful gateway connection.
	 *
	 * @param string $gateway_id Gateway ID.
	 */
	public static function handle_successfull_connection( $gateway_id ) {
		// phpcs:disable WordPress.Security.NonceVerification
		if ( ! isset( $_GET['success'] ) || 1 !== intval( $_GET['success'] ) ) {
			return;
		}
		// phpcs:enable WordPress.Security.NonceVerification

		$payment_gateways = WC()->payment_gateways()->payment_gateways();
		$payment_gateway  = isset( $payment_gateways[ $gateway_id ] ) ? $payment_gateways[ $gateway_id ] : null;

		if ( ! $payment_gateway ) {
			return;
		}

		$payment_gateway->update_option( 'enabled', 'yes' );

		TransientNotices::add(
			array(
				'user_id' => get_current_user_id(),
				'id'      => 'payment-gateway-connection-return-' . str_replace( ',', '-', $gateway_id ),
				'status'  => 'success',
				'content' => sprintf(
					/* translators: the title of the payment gateway */
					__( '%s connected successfully', 'woocommerce' ),
					$payment_gateway->method_title
				),
			)
		);

		wc_admin_record_tracks_event(
			'tasklist_payment_connect_method',
			array(
				'payment_method' => $gateway_id,
			)
		);

		wp_safe_redirect( wc_admin_url() );
	}
}
Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php000064400000001534152333011040021066 0ustar00<?php
/**
 * Evaluates the spec and returns a status.
 */

namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RuleEvaluator;

/**
 * Evaluates the spec and returns the evaluated suggestion.
 */
class EvaluateSuggestion {
	/**
	 * Evaluates the spec and returns the suggestion.
	 *
	 * @param object|array $spec The suggestion to evaluate.
	 * @return object The evaluated suggestion.
	 */
	public static function evaluate( $spec ) {
		$rule_evaluator = new RuleEvaluator();
		$suggestion     = is_array( $spec ) ? (object) $spec : clone $spec;

		if ( isset( $suggestion->is_visible ) ) {
			$is_visible             = $rule_evaluator->evaluate( $suggestion->is_visible );
			$suggestion->is_visible = $is_visible;
		}

		return $suggestion;
	}
}
Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php000064400000055425152333011040021707 0ustar00<?php
/**
 * Gets a list of fallback methods if remote fetching is disabled.
 */

namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Init as OnboardingTasks;

/**
 * Default Payment Gateways
 */
class DefaultPaymentGateways {

	/**
	 * Get default specs.
	 *
	 * @return array Default specs.
	 */
	public static function get_all() {
		return array(
			array(
				'id'                  => 'payfast',
				'title'               => __( 'PayFast', 'woocommerce' ),
				'content'             => __( 'The PayFast extension for WooCommerce enables you to accept payments by Credit Card and EFT via one of South Africa’s most popular payment gateways. No setup fees or monthly subscription costs. Selecting this extension will configure your store to use South African rands as the selected currency.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/payfast.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/payfast.png',
				'plugins'             => array( 'woocommerce-payfast-gateway' ),
				'is_visible'          => array(
					self::get_rules_for_countries( array( 'ZA', 'GH', 'NG' ) ),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array( 'ZA', 'GH', 'NG' ),
				'category_additional' => array(),
			),
			array(
				'id'                      => 'stripe',
				'title'                   => __( ' Stripe', 'woocommerce' ),
				'content'                 => __( 'Accept debit and credit cards in 135+ currencies, methods such as Alipay, and one-touch checkout with Apple Pay.', 'woocommerce' ),
				'image'                   => WC_ADMIN_IMAGES_FOLDER_URL . '/stripe.png',
				'image_72x72'             => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/stripe.png',
				'plugins'                 => array( 'woocommerce-gateway-stripe' ),
				'is_visible'              => array(
					// https://stripe.com/global.
					self::get_rules_for_countries(
						array( 'AU', 'AT', 'BE', 'BG', 'BR', 'CA', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HK', 'IN', 'IE', 'IT', 'JP', 'LV', 'LT', 'LU', 'MY', 'MT', 'MX', 'NL', 'NZ', 'NO', 'PL', 'PT', 'RO', 'SG', 'SK', 'SI', 'ES', 'SE', 'CH', 'GB', 'US', 'PR', 'HU', 'SL', 'ID', 'MY', 'SI', 'PR' )
					),
					self::get_rules_for_cbd( false ),
				),
				'category_other'          => array( 'AU', 'AT', 'BE', 'BG', 'BR', 'CA', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HK', 'IN', 'IE', 'IT', 'JP', 'LV', 'LT', 'LU', 'MY', 'MT', 'MX', 'NL', 'NZ', 'NO', 'PL', 'PT', 'RO', 'SG', 'SK', 'SI', 'ES', 'SE', 'CH', 'GB', 'US', 'PR', 'HU', 'SL', 'ID', 'MY', 'SI', 'PR' ),
				'category_additional'     => array(),
				'recommendation_priority' => 3,
			),
			array(
				'id'                  => 'paystack',
				'title'               => __( 'Paystack', 'woocommerce' ),
				'content'             => __( 'Paystack helps African merchants accept one-time and recurring payments online with a modern, safe, and secure payment gateway.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/paystack.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/paystack.png',
				'plugins'             => array( 'woo-paystack' ),
				'is_visible'          => array(
					self::get_rules_for_countries( array( 'ZA', 'GH', 'NG' ) ),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array( 'ZA', 'GH', 'NG' ),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'kco',
				'title'               => __( 'Klarna Checkout', 'woocommerce' ),
				'content'             => __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/klarna-black.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/klarna.png',
				'plugins'             => array( 'klarna-checkout-for-woocommerce' ),
				'is_visible'          => array(
					self::get_rules_for_countries( array( 'SE', 'FI', 'NO' ) ),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array( 'SE', 'FI', 'NO' ),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'klarna_payments',
				'title'               => __( 'Klarna Payments', 'woocommerce' ),
				'content'             => __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/klarna-black.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/klarna.png',
				'plugins'             => array( 'klarna-payments-for-woocommerce' ),
				'is_visible'          => array(
					self::get_rules_for_countries(
						array( 'US', 'CA', 'DK', 'DE', 'AT', 'NL', 'CH', 'BE', 'SP', 'PL', 'FR', 'IT', 'GB', 'ES', 'FI', 'NO', 'SE', 'ES', 'FI', 'NO', 'SE' )
					),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array(),
				'category_additional' => array( 'US', 'CA', 'DK', 'DE', 'AT', 'NL', 'CH', 'BE', 'SP', 'PL', 'FR', 'IT', 'GB', 'ES', 'FI', 'NO', 'SE', 'ES', 'FI', 'NO', 'SE' ),
			),
			array(
				'id'                  => 'mollie_wc_gateway_banktransfer',
				'title'               => __( 'Mollie', 'woocommerce' ),
				'content'             => __( 'Effortless payments by Mollie: Offer global and local payment methods, get onboarded in minutes, and supported in your language.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/mollie.svg',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/mollie.png',
				'plugins'             => array( 'mollie-payments-for-woocommerce' ),
				'is_visible'          => array(
					self::get_rules_for_countries(
						array( 'FR', 'DE', 'GB', 'AT', 'CH', 'ES', 'IT', 'PL', 'FI', 'NL', 'BE' )
					),
				),
				'category_other'      => array( 'FR', 'DE', 'GB', 'AT', 'CH', 'ES', 'IT', 'PL', 'FI', 'NL', 'BE' ),
				'category_additional' => array(),
			),
			array(
				'id'                      => 'woo-mercado-pago-custom',
				'title'                   => __( 'Mercado Pago Checkout Pro & Custom', 'woocommerce' ),
				'content'                 => __( 'Accept credit and debit cards, offline (cash or bank transfer) and logged-in payments with money in Mercado Pago. Safe and secure payments with the leading payment processor in LATAM.', 'woocommerce' ),
				'image'                   => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/mercadopago.png',
				'image_72x72'             => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/mercadopago.png',
				'plugins'                 => array( 'woocommerce-mercadopago' ),
				'is_visible'              => array(
					self::get_rules_for_countries( array( 'AR', 'BR', 'CL', 'CO', 'MX', 'PE', 'UY' ) ),
				),
				'recommendation_priority' => 2,
				'is_local_partner'        => true,
				'category_other'          => array( 'AR', 'BR', 'CL', 'CO', 'MX', 'PE', 'UY' ),
				'category_additional'     => array(),
			),
			array(
				'id'                  => 'ppcp-gateway',
				'title'               => __( 'PayPal Payments', 'woocommerce' ),
				'content'             => __( "Safe and secure payments using credit cards or your customer's PayPal account.", 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/paypal.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/paypal.png',
				'plugins'             => array( 'woocommerce-paypal-payments' ),
				'is_visible'          => array(
					(object) array(
						'type'      => 'base_location_country',
						'value'     => 'IN',
						'operation' => '!=',
					),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array( 'US', 'CA', 'AT', 'BE', 'BG', 'HR', 'CH', 'CY', 'CZ', 'DK', 'EE', 'ES', 'FI', 'FR', 'DE', 'GB', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SK', 'SL', 'SE', 'MX', 'BR', 'AR', 'CL', 'CO', 'EC', 'PE', 'UY', 'VE', 'AU', 'NZ', 'HK', 'JP', 'SG', 'CN', 'ID', 'ZA', 'NG', 'GH' ),
				'category_additional' => array( 'US', 'CA', 'AT', 'BE', 'BG', 'HR', 'CH', 'CY', 'CZ', 'DK', 'EE', 'ES', 'FI', 'FR', 'DE', 'GB', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SK', 'SL', 'SE', 'MX', 'BR', 'AR', 'CL', 'CO', 'EC', 'PE', 'UY', 'VE', 'AU', 'NZ', 'HK', 'JP', 'SG', 'CN', 'ID', 'IN', 'ZA', 'NG', 'GH' ),
			),
			array(
				'id'          => 'cod',
				'title'       => __( 'Cash on delivery', 'woocommerce' ),
				'content'     => __( 'Take payments in cash upon delivery.', 'woocommerce' ),
				'image'       => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/cod.svg',
				'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/cod.png',
				'is_visible'  => array(
					self::get_rules_for_cbd( false ),
				),
				'is_offline'  => true,
			),
			array(
				'id'          => 'bacs',
				'title'       => __( 'Direct bank transfer', 'woocommerce' ),
				'content'     => __( 'Take payments via bank transfer.', 'woocommerce' ),
				'image'       => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/bacs.svg',
				'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/bacs.png',
				'is_visible'  => array(
					self::get_rules_for_cbd( false ),
				),
				'is_offline'  => true,
			),
			// This is for backwards compatibility only (WC < 5.10.0-dev or WCA < 2.9.0-dev).
			array(
				'id'                      => 'woocommerce_payments',
				'title'                   => __( 'WooCommerce Payments', 'woocommerce' ),
				'content'                 => __(
					'Manage transactions without leaving your WordPress Dashboard. Only with WooCommerce Payments.',
					'woocommerce'
				),
				'image'                   => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg',
				'image_72x72'             => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg',
				'plugins'                 => array( 'woocommerce-payments' ),
				'description'             => __( 'With WooCommerce Payments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies. Track cash flow and manage recurring revenue directly from your store’s dashboard - with no setup costs or monthly fees.', 'woocommerce' ),
				'is_visible'              => array(
					self::get_rules_for_cbd( false ),
					self::get_rules_for_countries( self::get_wcpay_countries() ),
					(object) array(
						'type'     => 'plugin_version',
						'plugin'   => 'woocommerce',
						'version'  => '5.10.0-dev',
						'operator' => '<',
					),
					(object) array(
						'type'     => 'or',
						'operands' => (object) array(
							(object) array(
								'type'    => 'not',
								'operand' => [
									(object) array(
										'type'    => 'plugins_activated',
										'plugins' => [ 'woocommerce-admin' ],
									),
								],
							),
							(object) array(
								'type'     => 'plugin_version',
								'plugin'   => 'woocommerce-admin',
								'version'  => '2.9.0-dev',
								'operator' => '<',
							),
						),
					),
				),
				'recommendation_priority' => 1,
			),
			array(
				'id'                      => 'woocommerce_payments:without-in-person-payments',
				'title'                   => __( 'WooCommerce Payments', 'woocommerce' ),
				'content'                 => __(
					'Manage transactions without leaving your WordPress Dashboard. Only with WooCommerce Payments.',
					'woocommerce'
				),
				'image'                   => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg',
				'image_72x72'             => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg',
				'plugins'                 => array( 'woocommerce-payments' ),
				'description'             => __( 'With WooCommerce Payments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies. Track cash flow and manage recurring revenue directly from your store’s dashboard - with no setup costs or monthly fees.', 'woocommerce' ),
				'is_visible'              => array(
					self::get_rules_for_cbd( false ),
					self::get_rules_for_countries( array_diff( self::get_wcpay_countries(), array( 'US', 'CA' ) ) ),
					(object) array(
						'type'     => 'or',
						// Older versions of WooCommerce Admin require the ID to be `woocommerce-payments` to show the suggestion card.
						'operands' => (object) array(
							(object) array(
								'type'     => 'plugin_version',
								'plugin'   => 'woocommerce-admin',
								'version'  => '2.9.0-dev',
								'operator' => '>=',
							),
							(object) array(
								'type'     => 'plugin_version',
								'plugin'   => 'woocommerce',
								'version'  => '5.10.0-dev',
								'operator' => '>=',
							),
						),
					),
				),
				'recommendation_priority' => 1,
			),
			// This is the same as the above, but with a different description for countries that support in-person payments such as US and CA.
			array(
				'id'                      => 'woocommerce_payments:with-in-person-payments',
				'title'                   => __( 'WooCommerce Payments', 'woocommerce' ),
				'content'                 => __(
					'Manage transactions without leaving your WordPress Dashboard. Only with WooCommerce Payments.',
					'woocommerce'
				),
				'image'                   => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg',
				'image_72x72'             => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg',
				'plugins'                 => array( 'woocommerce-payments' ),
				'description'             => __( 'With WooCommerce Payments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies – with no setup costs or monthly fees – and you can now accept in-person payments with the Woo mobile app.', 'woocommerce' ),
				'is_visible'              => array(
					self::get_rules_for_cbd( false ),
					self::get_rules_for_countries( array( 'US', 'CA' ) ),
					(object) array(
						'type'     => 'or',
						// Older versions of WooCommerce Admin require the ID to be `woocommerce-payments` to show the suggestion card.
						'operands' => (object) array(
							(object) array(
								'type'     => 'plugin_version',
								'plugin'   => 'woocommerce-admin',
								'version'  => '2.9.0-dev',
								'operator' => '>=',
							),
							(object) array(
								'type'     => 'plugin_version',
								'plugin'   => 'woocommerce',
								'version'  => '5.10.0-dev',
								'operator' => '>=',
							),
						),
					),
				),
				'recommendation_priority' => 1,
			),
			array(
				'id'                  => 'razorpay',
				'title'               => __( 'Razorpay', 'woocommerce' ),
				'content'             => __( 'The official Razorpay extension for WooCommerce allows you to accept credit cards, debit cards, netbanking, wallet, and UPI payments.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/razorpay.svg',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/razorpay.png',
				'plugins'             => array( 'woo-razorpay' ),
				'is_visible'          => array(
					(object) array(
						'type'      => 'base_location_country',
						'value'     => 'IN',
						'operation' => '=',
					),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array( 'IN' ),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'payubiz',
				'title'               => __( 'PayU for WooCommerce', 'woocommerce' ),
				'content'             => __( 'Enable PayU’s exclusive plugin for WooCommerce to start accepting payments in 100+ payment methods available in India including credit cards, debit cards, UPI, & more!', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/payu.svg',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/payu.png',
				'plugins'             => array( 'payu-india' ),
				'is_visible'          => array(
					(object) array(
						'type'      => 'base_location_country',
						'value'     => 'IN',
						'operation' => '=',
					),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array( 'IN' ),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'eway',
				'title'               => __( 'Eway', 'woocommerce' ),
				'content'             => __( 'The Eway extension for WooCommerce allows you to take credit card payments directly on your store without redirecting your customers to a third party site to make payment.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/eway.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/eway.png',
				'plugins'             => array( 'woocommerce-gateway-eway' ),
				'is_visible'          => array(
					self::get_rules_for_countries( array( 'AU', 'NZ' ) ),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array( 'AU', 'NZ' ),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'square_credit_card',
				'title'               => __( 'Square', 'woocommerce' ),
				'content'             => __( 'Securely accept credit and debit cards with one low rate, no surprise fees (custom rates available). Sell online and in store and track sales and inventory in one place.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/square-black.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/square.png',
				'plugins'             => array( 'woocommerce-square' ),
				'is_visible'          => array(
					(object) array(
						'type'     => 'or',
						'operands' => (object) array(
							array(
								self::get_rules_for_countries( array( 'US' ) ),
								self::get_rules_for_cbd( true ),
							),
							array(
								self::get_rules_for_countries( array( 'US', 'CA', 'JP', 'GB', 'AU', 'IE', 'FR', 'ES', 'FI' ) ),
								self::get_rules_for_selling_venues( array( 'brick-mortar', 'brick-mortar-other' ) ),
							),
						),
					),
				),
				'category_other'      => array( 'US', 'CA', 'JP', 'GB', 'AU', 'IE', 'FR', 'ES', 'FI' ),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'afterpay',
				'title'               => __( 'Afterpay', 'woocommerce' ),
				'content'             => __( 'Afterpay allows customers to receive products immediately and pay for purchases over four installments, always interest-free.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/afterpay.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/afterpay.png',
				'plugins'             => array( 'afterpay-gateway-for-woocommerce' ),
				'is_visible'          => array(
					self::get_rules_for_countries( array( 'US', 'CA' ) ),
				),
				'category_other'      => array(),
				'category_additional' => array( 'US', 'CA' ),
			),
			array(
				'id'                  => 'amazon_payments_advanced',
				'title'               => __( 'Amazon Pay', 'woocommerce' ),
				'content'             => __( 'Enable a familiar, fast checkout for hundreds of millions of active Amazon customers globally.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/amazonpay.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/amazonpay.png',
				'plugins'             => array( 'woocommerce-gateway-amazon-payments-advanced' ),
				'is_visible'          => array(
					self::get_rules_for_countries( array( 'US', 'CA' ) ),
				),
				'category_other'      => array(),
				'category_additional' => array( 'US', 'CA' ),
			),
			array(
				'id'                  => 'affirm',
				'title'               => __( 'Affirm', 'woocommerce' ),
				'content'             => __( 'Affirm’s tailored Buy Now Pay Later programs remove price as a barrier, turning browsers into buyers, increasing average order value, and expanding your customer base.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/affirm.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/affirm.png',
				'plugins'             => array(),
				'external_link'       => 'https://woocommerce.com/products/woocommerce-gateway-affirm',
				'is_visible'          => array(
					self::get_rules_for_countries( array( 'US', 'CA' ) ),
				),
				'category_other'      => array(),
				'category_additional' => array( 'US', 'CA' ),
			),
		);
	}

	/**
	 * Get array of countries supported by WCPay depending on feature flag.
	 *
	 * @return array Array of countries.
	 */
	public static function get_wcpay_countries() {
		return array( 'US', 'PR', 'AU', 'CA', 'DE', 'ES', 'FR', 'GB', 'IE', 'IT', 'NZ', 'AT', 'BE', 'NL', 'PL', 'PT', 'CH', 'HK', 'SG' );
	}

	/**
	 * Get rules that match the store base location to one of the provided countries.
	 *
	 * @param array $countries Array of countries to match.
	 * @return object Rules to match.
	 */
	public static function get_rules_for_countries( $countries ) {
		$rules = array();

		foreach ( $countries as $country ) {
			$rules[] = (object) array(
				'type'      => 'base_location_country',
				'value'     => $country,
				'operation' => '=',
			);
		}

		return (object) array(
			'type'     => 'or',
			'operands' => $rules,
		);
	}

	/**
	 * Get rules that match the store's selling venues.
	 *
	 * @param array $selling_venues Array of venues to match.
	 * @return object Rules to match.
	 */
	public static function get_rules_for_selling_venues( $selling_venues ) {
		$rules = array();

		foreach ( $selling_venues as $venue ) {
			$rules[] = (object) array(
				'type'         => 'option',
				'transformers' => array(
					(object) array(
						'use'       => 'dot_notation',
						'arguments' => (object) array(
							'path' => 'selling_venues',
						),
					),
				),
				'option_name'  => 'woocommerce_onboarding_profile',
				'operation'    => '=',
				'value'        => $venue,
				'default'      => array(),
			);
		}

		return (object) array(
			'type'     => 'or',
			'operands' => $rules,
		);
	}

	/**
	 * Get default rules for CBD based on given argument.
	 *
	 * @param bool $should_have Whether or not the store should have CBD as an industry (true) or not (false).
	 * @return array Rules to match.
	 */
	public static function get_rules_for_cbd( $should_have ) {
		return (object) array(
			'type'         => 'option',
			'transformers' => array(
				(object) array(
					'use'       => 'dot_notation',
					'arguments' => (object) array(
						'path' => 'industry',
					),
				),
				(object) array(
					'use'       => 'array_column',
					'arguments' => (object) array(
						'key' => 'slug',
					),
				),
			),
			'option_name'  => 'woocommerce_onboarding_profile',
			'operation'    => $should_have ? 'contains' : '!contains',
			'value'        => 'cbd-other-hemp-derived-products',
			'default'      => array(),
		);
	}

}
Admin/Features/PaymentGatewaySuggestions/PaymentGatewaySuggestionsDataSourcePoller.php000064400000001504152333011040025570 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions;

use Automattic\WooCommerce\Admin\DataSourcePoller;

/**
 * Specs data source poller class for payment gateway suggestions.
 */
class PaymentGatewaySuggestionsDataSourcePoller extends DataSourcePoller {

	/**
	 * Data Source Poller ID.
	 */
	const ID = 'payment_gateway_suggestions';

	/**
	 * Default data sources array.
	 */
	const DATA_SOURCES = array(
		'https://woocommerce.com/wp-json/wccom/payment-gateway-suggestions/1.0/suggestions.json',
	);

	/**
	 * Class instance.
	 *
	 * @var Analytics instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self( self::ID, self::DATA_SOURCES );
		}
		return self::$instance;
	}
}
Admin/Features/TransientNotices.php000064400000005451152333011040013354 0ustar00<?php
/**
 * WooCommerce Transient Notices
 */

namespace Automattic\WooCommerce\Admin\Features;

use \Automattic\WooCommerce\Internal\Admin\Loader;

/**
 * Shows print shipping label banner on edit order page.
 */
class TransientNotices {

	/**
	 * Option name for the queue.
	 */
	const QUEUE_OPTION = 'woocommerce_admin_transient_notices_queue';

	/**
	 * Constructor
	 */
	public function __construct() {
		add_filter( 'woocommerce_admin_preload_options', array( $this, 'preload_options' ) );
	}


	/**
	 * Get all notices in the queue.
	 *
	 * @return array
	 */
	public static function get_queue() {
		return get_option( self::QUEUE_OPTION, array() );
	}

	/**
	 * Get all notices in the queue by a given user ID.
	 *
	 * @param int $user_id User ID.
	 * @return array
	 */
	public static function get_queue_by_user( $user_id ) {
		$notices = self::get_queue();

		return array_filter(
			$notices,
			function( $notice ) use ( $user_id ) {
				return ! isset( $notice['user_id'] ) ||
					null === $notice['user_id'] ||
					$user_id === $notice['user_id'];
			}
		);
	}

	/**
	 * Get a notice by ID.
	 *
	 * @param array $notice_id Notice of ID to get.
	 * @return array|null
	 */
	public static function get( $notice_id ) {
		$queue = self::get_queue();

		if ( isset( $queue[ $notice_id ] ) ) {
			return $queue[ $notice_id ];
		}

		return null;
	}

	/**
	 * Add a notice to be shown.
	 *
	 * @param array $notice Notice.
	 *    $notice = array(
	 *      'id'      => (string) Unique ID for the notice. Required.
	 *      'user_id' => (int|null) User ID to show the notice to.
	 *      'status'  => (string) info|error|success
	 *      'content' => (string) Content to be shown for the notice. Required.
	 *      'options' => (array) Array of options to be passed to the notice component.
	 *       See https://developer.wordpress.org/block-editor/reference-guides/data/data-core-notices/#createNotice for available options.
	 *    ).
	 */
	public static function add( $notice ) {
		$queue = self::get_queue();

		$defaults               = array(
			'user_id' => null,
			'status'  => 'info',
			'options' => array(),
		);
		$notice_data            = array_merge( $defaults, $notice );
		$notice_data['options'] = (object) $notice_data['options'];

		$queue[ $notice['id'] ] = $notice_data;
		update_option( self::QUEUE_OPTION, $queue );
	}

	/**
	 * Remove a notice by ID.
	 *
	 * @param array $notice_id Notice of ID to remove.
	 */
	public static function remove( $notice_id ) {
		$queue = self::get_queue();
		unset( $queue[ $notice_id ] );
		update_option( self::QUEUE_OPTION, $queue );
	}

	/**
	 * Preload options to prime state of the application.
	 *
	 * @param array $options Array of options to preload.
	 * @return array
	 */
	public function preload_options( $options ) {
		$options[] = self::QUEUE_OPTION;

		return $options;
	}

}
Admin/Features/MultichannelMarketing/Init.php000064400000004153152333011040015246 0ustar00<?php
/**
 * Multichannel Marketing Experience
 *
 * @package Woocommerce Admin
 */

namespace Automattic\WooCommerce\Admin\Features\MultichannelMarketing;

use Automattic\WooCommerce\Internal\Admin\Survey;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Contains logic for Multichannel Marketing.
 */
class Init {
	/**
	 * Option name used to toggle this feature.
	 */
	const TOGGLE_OPTION_NAME = 'woocommerce_multichannel_marketing_enabled';

	/**
	 * Determines if the feature has been toggled on or off.
	 *
	 * @var boolean
	 */
	protected static $is_updated = false;

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		add_filter( 'woocommerce_settings_features', array( $this, 'add_feature_toggle' ) );
		add_action( 'update_option_' . self::TOGGLE_OPTION_NAME, array( $this, 'reload_page_on_toggle' ), 10, 2 );
		add_action( 'woocommerce_settings_saved', array( $this, 'maybe_reload_page' ) );
	}

	/**
	 * Add the feature toggle to the features settings.
	 *
	 * @param array $features Feature sections.
	 * @return array
	 */
	public static function add_feature_toggle( $features ) {
		$description = __(
			'Enables the new WooCommerce Multichannel Marketing experience in the Marketing page',
			'woocommerce'
		);

		$features[] = array(
			'title' => __( 'Marketing', 'woocommerce' ),
			'desc'  => $description,
			'id'    => self::TOGGLE_OPTION_NAME,
			'type'  => 'checkbox',
			'class' => '',
		);

		return $features;
	}

	/**
	 * Reloads the page when the option is toggled to make sure all Multichannel Marketing features are loaded.
	 *
	 * @param string $old_value Old value.
	 * @param string $value     New value.
	 */
	public static function reload_page_on_toggle( $old_value, $value ) {
		if ( $old_value === $value ) {
			return;
		}

		self::$is_updated = true;
	}

	/**
	 * Reload the page if the setting has been updated.
	 */
	public static function maybe_reload_page() {
		if ( ! isset( $_SERVER['REQUEST_URI'] ) || ! self::$is_updated ) {
			return;
		}

		wp_safe_redirect( wp_unslash( $_SERVER['REQUEST_URI'] ) );
		exit();
	}
}
Admin/Features/Navigation/Favorites.php000064400000005026152333011040014117 0ustar00<?php
/**
 * WooCommerce Navigation Favorite
 *
 * @package Woocommerce Navigation
 */

namespace Automattic\WooCommerce\Admin\Features\Navigation;

use Automattic\WooCommerce\Internal\Admin\WCAdminUser;

/**
 * Contains logic for the WooCommerce Navigation menu.
 */
class Favorites {

	/**
	 * Array index of menu capability.
	 *
	 * @var int
	 */
	const META_NAME = 'navigation_favorites';

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Set given favorites string to the user meta data.
	 *
	 * @param string|number $user_id User id.
	 * @param array         $favorites Array of favorite values to set.
	 */
	private static function set_meta_value( $user_id, $favorites ) {
		WCAdminUser::update_user_data_field( $user_id, self::META_NAME, wp_json_encode( (array) $favorites ) );
	}

	/**
	 * Add item to favorites
	 *
	 * @param string        $item_id Identifier of item to add.
	 * @param string|number $user_id Identifier of user to add to.
	 * @return WP_Error|Boolean   Throws exception if item already exists.
	 */
	public static function add_item( $item_id, $user_id ) {

		$all_favorites = self::get_all( $user_id );

		if ( in_array( $item_id, $all_favorites, true ) ) {
			return new \WP_Error(
				'woocommerce_favorites_already_exists',
				__( 'Favorite already exists', 'woocommerce' )
			);
		}

		$all_favorites[] = $item_id;

		self::set_meta_value( $user_id, $all_favorites );

		return true;
	}

	/**
	 * Remove item from favorites
	 *
	 * @param string        $item_id Identifier of item to remove.
	 * @param string|number $user_id Identifier of user to remove from.
	 * @return \WP_Error|Boolean   Throws exception if item does not exist.
	 */
	public static function remove_item( $item_id, $user_id ) {
		$all_favorites = self::get_all( $user_id );

		if ( ! in_array( $item_id, $all_favorites, true ) ) {
			return new \WP_Error(
				'woocommerce_favorites_does_not_exist',
				__( 'Favorite item not found', 'woocommerce' )
			);
		}

		$remaining = array_values( array_diff( $all_favorites, [ $item_id ] ) );

		self::set_meta_value( $user_id, $remaining );

		return true;
	}

	/**
	 * Get all registered favorites.
	 *
	 * @param string|number $user_id Identifier of user to query.
	 * @return WP_Error|Array
	 */
	public static function get_all( $user_id ) {
		$response = WCAdminUser::get_user_data_field( $user_id, self::META_NAME );

		return $response ? json_decode( $response, true ) : array();
	}

}
Admin/Features/Navigation/Menu.php000064400000054435152333011040013071 0ustar00<?php
/**
 * WooCommerce Navigation Menu
 *
 * @package Woocommerce Navigation
 */

namespace Automattic\WooCommerce\Admin\Features\Navigation;

use Automattic\WooCommerce\Admin\Features\Navigation\Favorites;
use Automattic\WooCommerce\Admin\Features\Navigation\Screen;
use Automattic\WooCommerce\Admin\Features\Navigation\CoreMenu;

/**
 * Contains logic for the WooCommerce Navigation menu.
 */
class Menu {
	/**
	 * Class instance.
	 *
	 * @var Menu instance
	 */
	protected static $instance = null;

	/**
	 * Array index of menu capability.
	 *
	 * @var int
	 */
	const CAPABILITY = 1;

	/**
	 * Array index of menu callback.
	 *
	 * @var int
	 */
	const CALLBACK = 2;

	/**
	 * Array index of menu callback.
	 *
	 * @var int
	 */
	const SLUG = 3;

	/**
	 * Array index of menu CSS class string.
	 *
	 * @var int
	 */
	const CSS_CLASSES = 4;

	/**
	 * Array of usable menu IDs.
	 */
	const MENU_IDS = array(
		'primary',
		'favorites',
		'plugins',
		'secondary',
	);

	/**
	 * Store menu items.
	 *
	 * @var array
	 */
	protected static $menu_items = array();

	/**
	 * Store categories with menu item IDs.
	 *
	 * @var array
	 */
	protected static $categories = array(
		'woocommerce' => array(),
	);

	/**
	 * Registered callbacks or URLs with migration boolean as key value pairs.
	 *
	 * @var array
	 */
	protected static $callbacks = array();

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Init.
	 */
	public function init() {
		add_action( 'admin_menu', array( $this, 'add_core_items' ), 100 );
		add_filter( 'admin_enqueue_scripts', array( $this, 'enqueue_data' ), 20 );

		add_filter( 'admin_menu', array( $this, 'migrate_core_child_items' ), PHP_INT_MAX - 1 );
		add_filter( 'admin_menu', array( $this, 'migrate_menu_items' ), PHP_INT_MAX - 2 );
	}

	/**
	 * Convert a WordPress menu callback to a URL.
	 *
	 * @param string $callback Menu callback.
	 * @return string
	 */
	public static function get_callback_url( $callback ) {
		// Return the full URL.
		if ( strpos( $callback, 'http' ) === 0 ) {
			return $callback;
		}

		$pos  = strpos( $callback, '?' );
		$file = $pos > 0 ? substr( $callback, 0, $pos ) : $callback;
		if ( file_exists( ABSPATH . "/wp-admin/$file" ) ) {
			return $callback;
		}
		return 'admin.php?page=' . $callback;
	}

	/**
	 * Get the parent key if one exists.
	 *
	 * @param string $callback Callback or URL.
	 * @return string|null
	 */
	public static function get_parent_key( $callback ) {
		global $submenu;

		if ( ! $submenu ) {
			return null;
		}

		// This is already a parent item.
		if ( isset( $submenu[ $callback ] ) ) {
			return null;
		}

		foreach ( $submenu as $key => $menu ) {
			foreach ( $menu as $item ) {
				if ( $item[ self::CALLBACK ] === $callback ) {
					return $key;
				}
			}
		}

		return null;
	}

	/**
	 * Adds a top level menu item to the navigation.
	 *
	 * @param array $args Array containing the necessary arguments.
	 *    $args = array(
	 *      'id'      => (string) The unique ID of the menu item. Required.
	 *      'title'   => (string) Title of the menu item. Required.
	 *      'url'     => (string) URL or callback to be used. Required.
	 *      'order'   => (int) Menu item order.
	 *      'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
	 *      'menuId'  => (string) The ID of the menu to add the category to.
	 *    ).
	 */
	private static function add_category( $args ) {
		if ( ! isset( $args['id'] ) || isset( self::$menu_items[ $args['id'] ] ) ) {
			return;
		}

		$defaults           = array(
			'id'         => '',
			'title'      => '',
			'order'      => 100,
			'migrate'    => true,
			'menuId'     => 'primary',
			'isCategory' => true,
		);
		$menu_item          = wp_parse_args( $args, $defaults );
		$menu_item['title'] = wp_strip_all_tags( wp_specialchars_decode( $menu_item['title'] ) );
		unset( $menu_item['url'] );
		unset( $menu_item['capability'] );

		if ( ! isset( $menu_item['parent'] ) ) {
			$menu_item['parent']          = 'woocommerce';
			$menu_item['backButtonLabel'] = __(
				'WooCommerce Home',
				'woocommerce'
			);
		}

		self::$menu_items[ $menu_item['id'] ]       = $menu_item;
		self::$categories[ $menu_item['id'] ]       = array();
		self::$categories[ $menu_item['parent'] ][] = $menu_item['id'];

		if ( isset( $args['url'] ) ) {
			self::$callbacks[ $args['url'] ] = $menu_item['migrate'];
		}
	}

	/**
	 * Adds a child menu item to the navigation.
	 *
	 * @param array $args Array containing the necessary arguments.
	 *    $args = array(
	 *      'id'              => (string) The unique ID of the menu item. Required.
	 *      'title'           => (string) Title of the menu item. Required.
	 *      'parent'          => (string) Parent menu item ID.
	 *      'capability'      => (string) Capability to view this menu item.
	 *      'url'             => (string) URL or callback to be used. Required.
	 *      'order'           => (int) Menu item order.
	 *      'migrate'         => (bool) Whether or not to hide the item in the wp admin menu.
	 *      'menuId'          => (string) The ID of the menu to add the item to.
	 *      'matchExpression' => (string) A regular expression used to identify if the menu item is active.
	 *    ).
	 */
	private static function add_item( $args ) {
		if ( ! isset( $args['id'] ) ) {
			return;
		}

		if ( isset( self::$menu_items[ $args['id'] ] ) ) {
			error_log(  // phpcs:ignore
				sprintf(
					/* translators: 1: Duplicate menu item path. */
					esc_html__( 'You have attempted to register a duplicate item with WooCommerce Navigation: %1$s', 'woocommerce' ),
					'`' . $args['id'] . '`'
				)
			);
			return;
		}

		$defaults           = array(
			'id'         => '',
			'title'      => '',
			'capability' => 'manage_woocommerce',
			'url'        => '',
			'order'      => 100,
			'migrate'    => true,
			'menuId'     => 'primary',
		);
		$menu_item          = wp_parse_args( $args, $defaults );
		$menu_item['title'] = wp_strip_all_tags( wp_specialchars_decode( $menu_item['title'] ) );
		$menu_item['url']   = self::get_callback_url( $menu_item['url'] );

		if ( ! isset( $menu_item['parent'] ) ) {
			$menu_item['parent'] = 'woocommerce';
		}

		$menu_item['menuId'] = self::get_item_menu_id( $menu_item );

		self::$menu_items[ $menu_item['id'] ]       = $menu_item;
		self::$categories[ $menu_item['parent'] ][] = $menu_item['id'];

		if ( isset( $args['url'] ) ) {
			self::$callbacks[ $args['url'] ] = $menu_item['migrate'];
		}
	}

	/**
	 * Get an item's menu ID from its parent.
	 *
	 * @param array $item Item args.
	 * @return string
	 */
	public static function get_item_menu_id( $item ) {
		$favorites = Favorites::get_all( get_current_user_id() );
		if ( is_array( $favorites ) && ! empty( $favorites ) && in_array( $item['id'], $favorites, true ) ) {
			return 'favorites';
		}

		if ( isset( $item['parent'] ) && isset( self::$menu_items[ $item['parent'] ] ) ) {
			$menu_id = self::$menu_items[ $item['parent'] ]['menuId'];
			return 'favorites' === $menu_id
				? 'plugins'
				: $menu_id;
		}

		return $item['menuId'];
	}

	/**
	 * Adds a plugin category.
	 *
	 * @param array $args Array containing the necessary arguments.
	 *    $args = array(
	 *      'id'      => (string) The unique ID of the menu item. Required.
	 *      'title'   => (string) Title of the menu item. Required.
	 *      'url'     => (string) URL or callback to be used. Required.
	 *      'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
	 *      'order'   => (int) Menu item order.
	 *    ).
	 */
	public static function add_plugin_category( $args ) {
		$category_args = array_merge(
			$args,
			array(
				'menuId' => 'plugins',
			)
		);

		if ( ! isset( $category_args['parent'] ) ) {
			unset( $category_args['order'] );
		}

		$menu_id = self::get_item_menu_id( $category_args );
		if ( ! in_array( $menu_id, array( 'plugins', 'favorites' ), true ) ) {
			return;
		}

		$category_args['menuId'] = $menu_id;

		self::add_category( $category_args );
	}

	/**
	 * Adds a plugin item.
	 *
	 * @param array $args Array containing the necessary arguments.
	 *    $args = array(
	 *      'id'              => (string) The unique ID of the menu item. Required.
	 *      'title'           => (string) Title of the menu item. Required.
	 *      'parent'          => (string) Parent menu item ID.
	 *      'capability'      => (string) Capability to view this menu item.
	 *      'url'             => (string) URL or callback to be used. Required.
	 *      'migrate'         => (bool) Whether or not to hide the item in the wp admin menu.
	 *      'order'           => (int) Menu item order.
	 *      'matchExpression' => (string) A regular expression used to identify if the menu item is active.
	 *    ).
	 */
	public static function add_plugin_item( $args ) {
		if ( ! isset( $args['parent'] ) ) {
			unset( $args['order'] );
		}

		$item_args = array_merge(
			$args,
			array(
				'menuId' => 'plugins',
			)
		);

		$menu_id = self::get_item_menu_id( $item_args );

		if ( 'plugins' !== $menu_id ) {
			return;
		}

		self::add_item( $item_args );
	}

	/**
	 * Adds a plugin setting item.
	 *
	 * @param array $args Array containing the necessary arguments.
	 *    $args = array(
	 *      'id'         => (string) The unique ID of the menu item. Required.
	 *      'title'      => (string) Title of the menu item. Required.
	 *      'capability' => (string) Capability to view this menu item.
	 *      'url'        => (string) URL or callback to be used. Required.
	 *      'migrate'    => (bool) Whether or not to hide the item in the wp admin menu.
	 *    ).
	 */
	public static function add_setting_item( $args ) {
		unset( $args['order'] );

		if ( isset( $args['parent'] ) || isset( $args['menuId'] ) ) {
			error_log(  // phpcs:ignore
				sprintf(
					/* translators: 1: Duplicate menu item path. */
					esc_html__( 'The item ID %1$s attempted to register using an invalid option. The arguments `menuId` and `parent` are not allowed for add_setting_item()', 'woocommerce' ),
					'`' . $args['id'] . '`'
				)
			);
		}

		$item_args = array_merge(
			$args,
			array(
				'menuId' => 'secondary',
				'parent' => 'woocommerce-settings',
			)
		);

		self::add_item( $item_args );
	}



	/**
	 * Get menu item templates for a given post type.
	 *
	 * @param string $post_type Post type to add.
	 * @param array  $menu_args Arguments merged with the returned menu items.
	 * @return array
	 */
	public static function get_post_type_items( $post_type, $menu_args = array() ) {
		$post_type_object = get_post_type_object( $post_type );

		if ( ! $post_type_object || ! $post_type_object->show_in_menu ) {
			return;
		}

		$parent           = isset( $menu_args['parent'] ) ? $menu_args['parent'] . '-' : '';
		$match_expression = isset( $_GET['post'] ) && get_post_type( intval( $_GET['post'] ) ) === $post_type // phpcs:ignore WordPress.Security.NonceVerification
			? '(edit.php|post.php)'
			: null;

		return array(
			'default' => array_merge(
				array(
					'title'           => esc_attr( $post_type_object->labels->menu_name ),
					'capability'      => $post_type_object->cap->edit_posts,
					'id'              => $parent . $post_type,
					'url'             => "edit.php?post_type={$post_type}",
					'matchExpression' => $match_expression,
				),
				$menu_args
			),
			'all'     => array_merge(
				array(
					'title'           => esc_attr( $post_type_object->labels->all_items ),
					'capability'      => $post_type_object->cap->edit_posts,
					'id'              => "{$parent}{$post_type}-all-items",
					'url'             => "edit.php?post_type={$post_type}",
					'order'           => 10,
					'matchExpression' => $match_expression,
				),
				$menu_args
			),
			'new'     => array_merge(
				array(
					'title'      => esc_attr( $post_type_object->labels->add_new ),
					'capability' => $post_type_object->cap->create_posts,
					'id'         => "{$parent}{$post_type}-add-new",
					'url'        => "post-new.php?post_type={$post_type}",
					'order'      => 20,
				),
				$menu_args
			),
		);
	}

	/**
	 * Get menu item templates for a given taxonomy.
	 *
	 * @param string $taxonomy Taxonomy to add.
	 * @param array  $menu_args Arguments merged with the returned menu items.
	 * @return array
	 */
	public static function get_taxonomy_items( $taxonomy, $menu_args = array() ) {
		$taxonomy_object = get_taxonomy( $taxonomy );

		if ( ! $taxonomy_object || ! $taxonomy_object->show_in_menu ) {
			return;
		}

		$parent             = isset( $menu_args['parent'] ) ? $menu_args['parent'] . '-' : '';
		$product_type_query = ! empty( $taxonomy_object->object_type )
			? "&post_type={$taxonomy_object->object_type[0]}"
			: '';
		$match_expression   = 'term.php';                               // Match term.php pages.
		$match_expression  .= "(?=.*[?|&]taxonomy=${taxonomy}(&|$|#))"; // Lookahead to match a taxonomy URL param.
		$match_expression  .= '|';                                      // Or.
		$match_expression  .= 'edit-tags.php';                          // Match edit-tags.php pages.
		$match_expression  .= "(?=.*[?|&]taxonomy=${taxonomy}(&|$|#))"; // Lookahead to match a taxonomy URL param.

		return array(
			'default' => array_merge(
				array(
					'title'           => esc_attr( $taxonomy_object->labels->menu_name ),
					'capability'      => $taxonomy_object->cap->edit_terms,
					'id'              => $parent . $taxonomy,
					'url'             => "edit-tags.php?taxonomy={$taxonomy}{$product_type_query}",
					'matchExpression' => $match_expression,
				),
				$menu_args
			),
			'all'     => array_merge(
				array(
					'title'           => esc_attr( $taxonomy_object->labels->all_items ),
					'capability'      => $taxonomy_object->cap->edit_terms,
					'id'              => "{$parent}{$taxonomy}-all-items",
					'url'             => "edit-tags.php?taxonomy={$taxonomy}{$product_type_query}",
					'matchExpression' => $match_expression,
					'order'           => 10,
				),
				$menu_args
			),

		);
	}

	/**
	 * Add core menu items.
	 */
	public function add_core_items() {
		$categories = CoreMenu::get_categories();
		foreach ( $categories as $category ) {
			self::add_category( $category );
		}

		$items = CoreMenu::get_items();
		foreach ( $items as $item ) {
			if ( isset( $item['is_category'] ) && $item['is_category'] ) {
				self::add_category( $item );
			} else {
				self::add_item( $item );
			}
		}
	}

	/**
	 * Add an item or taxonomy.
	 *
	 * @param array $menu_item Menu item.
	 */
	public function add_item_and_taxonomy( $menu_item ) {
		if ( in_array( $menu_item[2], CoreMenu::get_excluded_items(), true ) ) {
			return;
		}

		$menu_item[2] = htmlspecialchars_decode( $menu_item[2] );

		// Don't add already added items.
		$callbacks = self::get_callbacks();
		if ( array_key_exists( $menu_item[2], $callbacks ) ) {
			return;
		}

		// Don't add these Product submenus because they are added elsewhere.
		if ( in_array( $menu_item[2], array( 'product_importer', 'product_exporter', 'product_attributes' ), true ) ) {
			return;
		}

		self::add_plugin_item(
			array(
				'title'      => $menu_item[0],
				'capability' => $menu_item[1],
				'id'         => sanitize_title( $menu_item[0] ),
				'url'        => $menu_item[2],
			)
		);

		// Determine if migrated items are a taxonomy or post_type. If they are, register them.
		$parsed_url   = wp_parse_url( $menu_item[2] );
		$query_string = isset( $parsed_url['query'] ) ? $parsed_url['query'] : false;

		if ( $query_string ) {
			$query = array();
			parse_str( $query_string, $query );

			if ( isset( $query['taxonomy'] ) ) {
				Screen::register_taxonomy( $query['taxonomy'] );
			} elseif ( isset( $query['post_type'] ) ) {
				Screen::register_post_type( $query['post_type'] );
			}
		}
	}

	/**
	 * Migrate any remaining WooCommerce child items.
	 *
	 * @param array $menu Menu items.
	 * @return array
	 */
	public function migrate_core_child_items( $menu ) {
		global $submenu;

		if ( ! isset( $submenu['woocommerce'] ) && ! isset( $submenu['edit.php?post_type=product'] ) ) {
			return $menu;
		}

		$main_items    = isset( $submenu['woocommerce'] ) ? $submenu['woocommerce'] : array();
		$product_items = isset( $submenu['edit.php?post_type=product'] ) ? $submenu['edit.php?post_type=product'] : array();

		foreach ( $main_items as $key => $menu_item ) {
			self::add_item_and_taxonomy( $menu_item );
			// phpcs:disable
			if ( ! isset( $menu_item[ self::CSS_CLASSES ] ) ) {
				$submenu['woocommerce'][ $key ][] .= ' hide-if-js';
			} else if ( strpos( $submenu['woocommerce'][ $key ][ self::CSS_CLASSES ], 'hide-if-js' ) !== false ) {
				continue;
			} else {
				$submenu['woocommerce'][ $key ][ self::CSS_CLASSES ] .= ' hide-if-js';
			}
			// phpcs:enable
		}

		foreach ( $product_items as $key => $menu_item ) {
			self::add_item_and_taxonomy( $menu_item );
		}

		return $menu;
	}

	/**
	 * Check if a menu item's callback is registered in the menu.
	 *
	 * @param array $menu_item Menu item args.
	 * @return bool
	 */
	public static function has_callback( $menu_item ) {
		if ( ! $menu_item || ! isset( $menu_item[ self::CALLBACK ] ) ) {
			return false;
		}

		$callback = $menu_item[ self::CALLBACK ];

		if (
			isset( self::$callbacks[ $callback ] ) &&
			self::$callbacks[ $callback ]
		) {
			return true;
		}

		if (
			isset( self::$callbacks[ self::get_callback_url( $callback ) ] ) &&
			self::$callbacks[ self::get_callback_url( $callback ) ]
		) {
			return true;
		}

		return false;
	}

	/**
	 * Hides all WP admin menus items and adds screen IDs to check for new items.
	 */
	public static function migrate_menu_items() {
		global $menu, $submenu;

		foreach ( $menu as $key => $menu_item ) {
			if ( self::has_callback( $menu_item ) ) {
				// Disable phpcs since we need to override submenu classes.
				// Note that `phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited` does not work to disable this check.
				// phpcs:disable
				$menu[ $key ][ self::CSS_CLASSES ] .= ' hide-if-js';
				// phps:enable
				continue;
			}

			// WordPress core menus make the parent item the same URL as the first child.
			$has_children = isset( $submenu[ $menu_item[ self::CALLBACK ] ] ) && isset( $submenu[ $menu_item[ self::CALLBACK ] ][0] );
			$first_child  = $has_children ? $submenu[ $menu_item[ self::CALLBACK ] ][0] : null;
			if ( 'woocommerce' !== $menu_item[2] && self::has_callback( $first_child ) ) {
				// Disable phpcs since we need to override submenu classes.
				// Note that `phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited` does not work to disable this check.
				// phpcs:disable
				$menu[ $key ][ self::CSS_CLASSES ] .= ' hide-if-js';
				// phps:enable
			}
		}

		// Remove excluded submenu items
		if ( isset( $submenu['woocommerce'] ) ) {
			foreach ( $submenu['woocommerce'] as $key => $submenu_item ) {
				if ( in_array( $submenu_item[ self::CALLBACK ], CoreMenu::get_excluded_items(), true ) ) {
					if ( isset( $submenu['woocommerce'][ $key ][ self::CSS_CLASSES ] ) ) {
						$submenu['woocommerce'][ $key ][ self::CSS_CLASSES ] .= ' hide-if-js';
					} else {
						$submenu['woocommerce'][ $key ][] = 'hide-if-js';
					}
				}
			}
		}

		foreach ( $submenu as $parent_key => $parent ) {
			foreach ( $parent as $key => $menu_item ) {
				if ( self::has_callback( $menu_item ) ) {
					// Disable phpcs since we need to override submenu classes.
					// Note that `phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited` does not work to disable this check.
					// phpcs:disable
					if ( ! isset( $menu_item[ self::SLUG ] ) ) {
						$submenu[ $parent_key ][ $key ][] = '';
					}
					if ( ! isset( $menu_item[ self::CSS_CLASSES ] ) ) {
						$submenu[ $parent_key ][ $key ][] .= ' hide-if-js';
					} else {
						$submenu[ $parent_key ][ $key ][ self::CSS_CLASSES ] .= ' hide-if-js';
					}
					// phps:enable
				}
			}
		}

		foreach ( array_keys( self::$callbacks ) as $callback ) {
			Screen::add_screen( $callback );
		}
	}

	/**
	 * Add a callback to identify and hide pages in the WP menu.
	 */
	public static function hide_wp_menu_item( $callback ) {
		self::$callbacks[ $callback ] = true;
	}

	/**
	 * Get registered menu items.
	 *
	 * @return array
	 */
	public static function get_items() {
		return apply_filters( 'woocommerce_navigation_menu_items', self::$menu_items );
	}

	/**
	 * Get registered menu items.
	 *
	 * @return array
	 */
	public static function get_category_items( $category ) {
		if ( ! isset( self::$categories[ $category ] ) ) {
			return array();
		}

		$menu_item_ids = self::$categories[ $category ];

		$category_menu_items = array();
		foreach ( $menu_item_ids as $id ) {
			if ( isset( self::$menu_items[ $id ] ) ) {
				$category_menu_items[] = self::$menu_items[ $id ];
			}
		}

		return apply_filters( 'woocommerce_navigation_menu_category_items', $category_menu_items );
	}

	/**
	 * Get registered callbacks.
	 *
	 * @return array
	 */
	public static function get_callbacks() {
		return apply_filters( 'woocommerce_navigation_callbacks', self::$callbacks );
	}

	/**
	 * Gets the menu item data mapped by category and menu ID.
	 *
	 * @return array
	 */
	public static function get_mapped_menu_items() {
		$menu_items   = self::get_items();
		$mapped_items = array();

		// Sort the items by order and title.
		$order     = array_column( $menu_items, 'order' );
		$title     = array_column( $menu_items, 'title' );
		array_multisort( $order, SORT_ASC, $title, SORT_ASC, $menu_items );

		foreach ( $menu_items as $id => $menu_item ) {
			$category_id = $menu_item[ 'parent' ];
			$menu_id     = $menu_item[ 'menuId' ];
			if ( ! isset( $mapped_items[ $category_id ] ) ) {
				$mapped_items[ $category_id ] = array();
				foreach ( self::MENU_IDS as $available_menu_id ) {
					$mapped_items[ $category_id ][ $available_menu_id ] = array();
				}
			}

			// Incorrect menu ID.
			if ( ! isset( $mapped_items[ $category_id ][ $menu_id ] ) ) {
				continue;
			}

			// Remove the item if the user cannot access it.
			if ( isset( $menu_item[ 'capability' ] ) && ! current_user_can( $menu_item[ 'capability' ] ) ) {
				continue;
			}

			$mapped_items[ $category_id ][ $menu_id ][] = $menu_item;
		}

		return $mapped_items;
	}

	/**
	 * Add the menu to the page output.
	 *
	 * @param array $menu Menu items.
	 * @return array
	 */
	public function enqueue_data( $menu ) {
		$data = array(
			'menuItems'     => array_values( self::get_items() ),
			'rootBackUrl'   => get_dashboard_url(),
		);

		wp_add_inline_script( WC_ADMIN_APP, 'window.wcNavigation = ' . wp_json_encode( $data ), 'before' );
	}
}
Admin/Features/Navigation/Screen.php000064400000013106152333011040013372 0ustar00<?php
/**
 * WooCommerce Navigation Screen
 *
 * @package Woocommerce Navigation
 */

namespace Automattic\WooCommerce\Admin\Features\Navigation;

use Automattic\WooCommerce\Admin\Features\Navigation\Menu;

/**
 * Contains logic for the WooCommerce Navigation menu.
 */
class Screen {
	/**
	 * Class instance.
	 *
	 * @var Screen instance
	 */
	protected static $instance = null;

	/**
	 * Screen IDs of registered pages.
	 *
	 * @var array
	 */
	protected static $screen_ids = array();

	/**
	 * Registered post types.
	 *
	 * @var array
	 */
	protected static $post_types = array();

	/**
	 * Registered taxonomies.
	 *
	 * @var array
	 */
	protected static $taxonomies = array();

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Init.
	 */
	public function init() {
		add_filter( 'admin_body_class', array( $this, 'add_body_class' ) );
	}

	/**
	 * Returns an array of filtered screen ids.
	 */
	public static function get_screen_ids() {
		return apply_filters( 'woocommerce_navigation_screen_ids', self::$screen_ids );
	}

	/**
	 * Returns an array of registered post types.
	 */
	public static function get_post_types() {
		return apply_filters( 'woocommerce_navigation_post_types', self::$post_types );
	}

	/**
	 * Returns an array of registered post types.
	 */
	public static function get_taxonomies() {
		return apply_filters( 'woocommerce_navigation_taxonomies', self::$taxonomies );
	}

	/**
	 * Check if we're on a WooCommerce page
	 *
	 * @return bool
	 */
	public static function is_woocommerce_page() {
		global $pagenow;

		// Get taxonomy if on a taxonomy screen.
		$taxonomy = '';
		if ( in_array( $pagenow, array( 'edit-tags.php', 'term.php' ), true ) ) {
			if ( isset( $_GET['taxonomy'] ) ) { // phpcs:ignore CSRF ok.
				$taxonomy = sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) ); // phpcs:ignore CSRF ok.
			}
		}
		$taxonomies = self::get_taxonomies();

		// Get post type if on a post screen.
		$post_type = '';
		if ( in_array( $pagenow, array( 'edit.php', 'post.php', 'post-new.php' ), true ) ) {
			if ( isset( $_GET['post'] ) ) { // phpcs:ignore CSRF ok.
				$post_type = get_post_type( (int) $_GET['post'] ); // phpcs:ignore CSRF ok.
			} elseif ( isset( $_GET['post_type'] ) ) { // phpcs:ignore CSRF ok.
				$post_type = sanitize_text_field( wp_unslash( $_GET['post_type'] ) ); // phpcs:ignore CSRF ok.
			}
		}
		$post_types = self::get_post_types();

		// Get current screen ID.
		$current_screen    = get_current_screen();
		$screen_ids        = self::get_screen_ids();
		$current_screen_id = $current_screen ? $current_screen->id : null;

		if (
			in_array( $post_type, $post_types, true ) ||
			in_array( $taxonomy, $taxonomies, true ) ||
			self::is_woocommerce_core_taxonomy( $taxonomy ) ||
			in_array( $current_screen_id, $screen_ids, true )
		) {
			return true;
		}

		return false;
	}

	/**
	 * Check if a given taxonomy is a WooCommerce core related taxonomy.
	 *
	 * @param string $taxonomy Taxonomy.
	 * @return bool
	 */
	public static function is_woocommerce_core_taxonomy( $taxonomy ) {
		if ( in_array( $taxonomy, array( 'product_cat', 'product_tag' ), true ) ) {
			return true;
		}

		if ( 'pa_' === substr( $taxonomy, 0, 3 ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Add navigation classes to body.
	 *
	 * @param string $classes Classes.
	 * @return string
	 */
	public function add_body_class( $classes ) {
		if ( self::is_woocommerce_page() ) {
			$classes .= ' has-woocommerce-navigation';

			/**
			 * Adds the ability to skip disabling of the WP toolbar.
			 *
			 * @param boolean $bool WP Toolbar disabled.
			 */
			if ( apply_filters( 'woocommerce_navigation_wp_toolbar_disabled', true ) ) {
				$classes .= ' is-wp-toolbar-disabled';
			}
		}

		return $classes;
	}

	/**
	 * Adds a screen ID to the list of screens that use the navigtion.
	 * Finds the parent if none is given to grab the correct screen ID.
	 *
	 * @param string      $callback Callback or URL for page.
	 * @param string|null $parent   Parent screen ID.
	 */
	public static function add_screen( $callback, $parent = null ) {
		global $submenu;

		$plugin_page = self::get_plugin_page( $callback );

		if ( ! $parent ) {
			$parent = Menu::get_parent_key( $callback );
		}

		$screen_id = get_plugin_page_hookname( $plugin_page, $parent );

		// This screen has already been added.
		if ( in_array( $screen_id, self::$screen_ids, true ) ) {
			return;
		}

		self::$screen_ids[] = $screen_id;
	}

	/**
	 * Get the plugin page slug.
	 *
	 * @param string $callback Callback.
	 * @return string
	 */
	public static function get_plugin_page( $callback ) {
		$url   = Menu::get_callback_url( $callback );
		$parts = wp_parse_url( $url );

		if ( ! isset( $parts['query'] ) ) {
			return $callback;
		}

		parse_str( $parts['query'], $query );

		if ( ! isset( $query['page'] ) ) {
			return $callback;
		}

		$plugin_page = wp_unslash( $query['page'] );
		$plugin_page = plugin_basename( $plugin_page );
		return $plugin_page;
	}

	/**
	 * Register post type for use in WooCommerce Navigation screens.
	 *
	 * @param string $post_type Post type to add.
	 */
	public static function register_post_type( $post_type ) {
		if ( ! in_array( $post_type, self::$post_types, true ) ) {
			self::$post_types[] = $post_type;
		}
	}

	/**
	 * Register taxonomy for use in WooCommerce Navigation screens.
	 *
	 * @param string $taxonomy Taxonomy to add.
	 */
	public static function register_taxonomy( $taxonomy ) {
		if ( ! in_array( $taxonomy, self::$taxonomies, true ) ) {
			self::$taxonomies[] = $taxonomy;
		}
	}
}
Admin/Features/Navigation/CoreMenu.php000064400000030141152333011040013666 0ustar00<?php
/**
 * WooCommerce Navigation Core Menu
 *
 * @package Woocommerce Admin
 */

namespace Automattic\WooCommerce\Admin\Features\Navigation;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\Navigation\Menu;
use Automattic\WooCommerce\Admin\Features\Navigation\Screen;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;

/**
 * CoreMenu class. Handles registering Core menu items.
 */
class CoreMenu {
	/**
	 * Class instance.
	 *
	 * @var Menu instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Init.
	 */
	public function init() {
		add_action( 'admin_menu', array( $this, 'register_post_types' ) );
		// Add this after we've finished migrating menu items to avoid hiding these items.
		add_action( 'admin_menu', array( $this, 'add_dashboard_menu_items' ), PHP_INT_MAX );
	}

	/**
	 * Add registered admin settings as menu items.
	 */
	public static function get_setting_items() {
		// Let the Settings feature add pages to the navigation if enabled.
		if ( Features::is_enabled( 'settings' ) ) {
			return array();
		}

		// Calling this method adds pages to the below tabs filter on non-settings pages.
		\WC_Admin_Settings::get_settings_pages();
		$tabs = apply_filters( 'woocommerce_settings_tabs_array', array() );

		$menu_items = array();
		$order      = 0;
		foreach ( $tabs as $key => $setting ) {
			$order       += 10;
			$menu_items[] = (
			array(
				'parent'     => 'woocommerce-settings',
				'title'      => $setting,
				'capability' => 'manage_woocommerce',
				'id'         => 'settings-' . $key,
				'url'        => 'admin.php?page=wc-settings&tab=' . $key,
				'order'      => $order,
			)
			);
		}

		return $menu_items;
	}

	/**
	 * Get unfulfilled order count
	 *
	 * @return array
	 */
	public static function get_shop_order_count() {
		$status_counts = array_map( 'wc_orders_count', array( 'processing', 'on-hold' ) );
		return array_sum( $status_counts );
	}

	/**
	 * Get all menu categories.
	 *
	 * @return array
	 */
	public static function get_categories() {
		$analytics_enabled = Features::is_enabled( 'analytics' );
		return array(
			array(
				'title' => __( 'Orders', 'woocommerce' ),
				'id'    => 'woocommerce-orders',
				'badge' => self::get_shop_order_count(),
				'order' => 10,
			),
			array(
				'title' => __( 'Products', 'woocommerce' ),
				'id'    => 'woocommerce-products',
				'order' => 20,
			),
			$analytics_enabled ?
				array(
					'title' => __( 'Analytics', 'woocommerce' ),
					'id'    => 'woocommerce-analytics',
					'order' => 30,
				) : null,
			$analytics_enabled ?
				array(
					'title'  => __( 'Reports', 'woocommerce' ),
					'id'     => 'woocommerce-reports',
					'parent' => 'woocommerce-analytics',
					'order'  => 200,
				) : null,
			array(
				'title' => __( 'Marketing', 'woocommerce' ),
				'id'    => 'woocommerce-marketing',
				'order' => 40,
			),
			array(
				'title'  => __( 'Settings', 'woocommerce' ),
				'id'     => 'woocommerce-settings',
				'menuId' => 'secondary',
				'order'  => 20,
				'url'    => 'admin.php?page=wc-settings',
			),
			array(
				'title'  => __( 'Tools', 'woocommerce' ),
				'id'     => 'woocommerce-tools',
				'menuId' => 'secondary',
				'order'  => 30,
			),
		);
	}

	/**
	 * Get all menu items.
	 *
	 * @return array
	 */
	public static function get_items() {
		$order_items       = self::get_order_menu_items();
		$product_items     = Menu::get_post_type_items( 'product', array( 'parent' => 'woocommerce-products' ) );
		$product_tag_items = Menu::get_taxonomy_items(
			'product_tag',
			array(
				'parent' => 'woocommerce-products',
				'order'  => 30,
			)
		);
		$product_cat_items = Menu::get_taxonomy_items(
			'product_cat',
			array(
				'parent' => 'woocommerce-products',
				'order'  => 20,
			)
		);

		$coupon_items  = Menu::get_post_type_items( 'shop_coupon', array( 'parent' => 'woocommerce-marketing' ) );
		$setting_items = self::get_setting_items();
		$wca_items     = array();
		$wca_pages     = \Automattic\WooCommerce\Admin\PageController::get_instance()->get_pages();

		foreach ( $wca_pages as $page ) {
			if ( ! isset( $page['nav_args'] ) ) {
				continue;
			}

			$path = isset( $page['path'] ) ? $page['path'] : null;
			$item = array_merge(
				array(
					'id'         => $page['id'],
					'url'        => $path,
					'title'      => $page['title'][0],
					'capability' => isset( $page['capability'] ) ? $page['capability'] : 'manage_woocommerce',
				),
				$page['nav_args']
			);

			// Don't allow top-level items to be added to the primary menu.
			if ( ! isset( $item['parent'] ) || 'woocommerce' === $item['parent'] ) {
				$item['menuId'] = 'plugins';
			}

			$wca_items[] = $item;
		}

		$home_item = array();
		$setup_tasks_remaining = TaskLists::setup_tasks_remaining();
		if ( defined( '\Automattic\WooCommerce\Internal\Admin\Homescreen::MENU_SLUG' ) ) {
			$home_item = array(
				'id'              => 'woocommerce-home',
				'title'           => __( 'Home', 'woocommerce' ),
				'url'             => \Automattic\WooCommerce\Internal\Admin\Homescreen::MENU_SLUG,
				'order'           => 0,
				'matchExpression' => 'page=wc-admin((?!path=).)*$',
				'badge'           => $setup_tasks_remaining ? $setup_tasks_remaining : null,
			);
		}

		$customers_item = array();
		if ( Features::is_enabled( 'analytics' ) ) {
			$customers_item = array(
				'id'    => 'woocommerce-analytics-customers',
				'title' => __( 'Customers', 'woocommerce' ),
				'url'   => 'wc-admin&path=/customers',
				'order' => 50,
			);
		}

		$add_product_mvp = array();
		if ( Features::is_enabled( 'new-product-management-experience' ) ) {
			$add_product_mvp = array(
				'id'     => 'woocommerce-add-product-mbp',
				'title'  => __( 'Add New (MVP)', 'woocommerce' ),
				'url'    => 'admin.php?page=wc-admin&path=/add-product',
				'parent' => 'woocommerce-products',
				'order'  => 50,
			);
		}

		return array_merge(
			array(
				$home_item,
				$customers_item,
				$order_items['all'],
				$order_items['new'],
				$product_items['all'],
				$product_cat_items['default'],
				$product_tag_items['default'],
				array(
					'id'              => 'woocommerce-product-attributes',
					'title'           => __( 'Attributes', 'woocommerce' ),
					'url'             => 'edit.php?post_type=product&page=product_attributes',
					'capability'      => 'manage_product_terms',
					'order'           => 40,
					'parent'          => 'woocommerce-products',
					'matchExpression' => 'edit.php(?=.*[?|&]page=product_attributes(&|$|#))|edit-tags.php(?=.*[?|&]taxonomy=pa_)(?=.*[?|&]post_type=product(&|$|#))',
				),
				array_merge( $product_items['new'], array( 'order' => 50 ) ),
				$coupon_items['default'],
				// Marketplace category.
				array(
					'title'      => __( 'Marketplace', 'woocommerce' ),
					'capability' => 'manage_woocommerce',
					'id'         => 'woocommerce-marketplace',
					'url'        => 'wc-addons',
					'menuId'     => 'secondary',
					'order'      => 10,
				),
				$add_product_mvp,
			),
			// Tools category.
			self::get_tool_items(),
			// WooCommerce Admin items.
			$wca_items,
			// Settings category.
			$setting_items,
			// Legacy report items.
			self::get_legacy_report_items()
		);
	}

	/**
	 * Supplies menu items for orders.
	 *
	 * This varies depending on whether we are actively using traditional post type-based orders or the new custom
	 * table-based orders.
	 *
	 * @return ?array
	 */
	private static function get_order_menu_items(): ?array {
		if ( ! wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) {
			return Menu::get_post_type_items( 'shop_order', array( 'parent' => 'woocommerce-orders' ) );
		}

		$main_orders_menu = array(
			'title'      => __( 'Orders', 'woocommerce' ),
			'capability' => 'edit_others_shop_orders',
			'id'         => 'woocommerce-orders-default',
			'url'        => 'admin.php?page=wc-orders',
			'parent'     => 'woocommerce-orders',
		);

		$all_orders_entry          = $main_orders_menu;
		$all_orders_entry['id']    = 'woocommerce-orders-all-items';
		$all_orders_entry['order'] = 10;

		$new_orders_entry          = $main_orders_menu;
		$new_orders_entry['title'] = __( 'Add order', 'woocommerce' );
		$new_orders_entry['id']    = 'woocommerce-orders-add-item';
		$new_orders_entry['url']   = 'admin.php?page=TBD';
		$new_orders_entry['order'] = 20;

		return array(
			'default' => $main_orders_menu,
			'all'     => $all_orders_entry,
			'new'     => $new_orders_entry,
		);
	}

	/**
	 * Get items for tools category.
	 *
	 * @return array
	 */
	public static function get_tool_items() {
		$tabs = array(
			'status' => __( 'System status', 'woocommerce' ),
			'tools'  => __( 'Utilities', 'woocommerce' ),
			'logs'   => __( 'Logs', 'woocommerce' ),
		);
		$tabs = apply_filters( 'woocommerce_admin_status_tabs', $tabs );

		$order = 1;
		$items = array(
			array(
				'parent'     => 'woocommerce-tools',
				'title'      => __( 'Import / Export', 'woocommerce' ),
				'capability' => 'import',
				'id'         => 'tools-import-export',
				'url'        => 'import.php',
				'migrate'    => false,
				'order'      => 0,
			),
		);

		foreach ( $tabs as $key => $tab ) {
			$items[] = array(
				'parent'     => 'woocommerce-tools',
				'title'      => $tab,
				'capability' => 'manage_woocommerce',
				'id'         => 'tools-' . $key,
				'url'        => 'wc-status&tab=' . $key,
				'order'      => $order,
			);
			$order++;
		}

		return $items;
	}

	/**
	 * Get legacy report items.
	 *
	 * @return array
	 */
	public static function get_legacy_report_items() {
		$reports    = \WC_Admin_Reports::get_reports();
		$menu_items = array();

		$order = 0;
		foreach ( $reports as $key => $report ) {
			$menu_items[] = array(
				'parent'     => 'woocommerce-reports',
				'title'      => $report['title'],
				'capability' => 'view_woocommerce_reports',
				'id'         => $key,
				'url'        => 'wc-reports&tab=' . $key,
				'order'      => $order,
			);
			$order++;
		}

		return $menu_items;
	}

	/**
	 * Register all core post types.
	 */
	public function register_post_types() {
		Screen::register_post_type( 'shop_order' );
		Screen::register_post_type( 'product' );
		Screen::register_post_type( 'shop_coupon' );
	}

	/**
	 * Add the dashboard items to the WP menu to create a quick-access flyout menu.
	 */
	public function add_dashboard_menu_items() {
		global $submenu, $menu;
		$mapped_items = Menu::get_mapped_menu_items();
		$top_level    = $mapped_items['woocommerce'];

		// phpcs:disable
		if ( ! isset( $submenu['woocommerce'] ) || empty( $top_level ) ) {
			return;
		}

		$menuIds = array(
			'primary',
			'secondary',
			'favorites',
		);

		foreach ( $menuIds as $menuId ) {
			foreach( $top_level[ $menuId ] as $item ) {
				// Skip specific categories.
				if (
					in_array(
						$item['id'],
						array(
							'woocommerce-tools',
						),
						true
					)
				) {
					continue;
				}

				// Use the link from the first item if it's a category.
				if ( ! isset( $item['url'] ) ) {
					$categoryMenuId = $menuId === 'favorites' ? 'plugins' : $menuId;
					$category_items = $mapped_items[ $item['id'] ][ $categoryMenuId ];

					if ( ! empty( $category_items ) ) {
						$first_item = $category_items[0];


						$submenu['woocommerce'][] = array(
							$item['title'],
							$first_item['capability'],
							isset( $first_item['url'] ) ? $first_item['url'] : null,
							$item['title'],
						);
					}

					continue;
				}

				// Show top-level items.
				$submenu['woocommerce'][] = array(
					$item['title'],
					$item['capability'],
					isset( $item['url'] ) ? $item['url'] : null,
					$item['title'],
				);
			}
		}
		// phpcs:enable
	}

	/**
	 * Get items excluded from WooCommerce menu migration.
	 *
	 * @return array
	 */
	public static function get_excluded_items() {
		$excluded_items = array(
			'woocommerce',
			'wc-reports',
			'wc-settings',
			'wc-status',
		);

		return apply_filters( 'woocommerce_navigation_core_excluded_items', $excluded_items );
	}
}
Admin/Features/Navigation/Init.php000064400000007714152333011040013066 0ustar00<?php
/**
 * Navigation Experience
 *
 * @package Woocommerce Admin
 */

namespace Automattic\WooCommerce\Admin\Features\Navigation;

use Automattic\WooCommerce\Internal\Admin\Survey;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\Navigation\Screen;
use Automattic\WooCommerce\Admin\Features\Navigation\Menu;
use Automattic\WooCommerce\Admin\Features\Navigation\CoreMenu;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Contains logic for the Navigation
 */
class Init {
	/**
	 * Option name used to toggle this feature.
	 */
	const TOGGLE_OPTION_NAME = 'woocommerce_navigation_enabled';

	/**
	 * Determines if the feature has been toggled on or off.
	 *
	 * @var boolean
	 */
	protected static $is_updated = false;

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		add_action( 'update_option_' . self::TOGGLE_OPTION_NAME, array( $this, 'reload_page_on_toggle' ), 10, 2 );
		add_action( 'woocommerce_settings_saved', array( $this, 'maybe_reload_page' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_opt_out_scripts' ) );

		if ( Features::is_enabled( 'navigation' ) ) {
			Menu::instance()->init();
			CoreMenu::instance()->init();
			Screen::instance()->init();
		}
	}

	/**
	 * Add the feature toggle to the features settings.
	 *
	 * @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class).
	 *
	 * @param array $features Feature sections.
	 * @return array
	 */
	public static function add_feature_toggle( $features ) {
		return $features;
	}

	/**
	 * Determine if sufficient versions are present to support Navigation feature
	 */
	public function is_nav_compatible() {
		include_once ABSPATH . 'wp-admin/includes/plugin.php';

		$gutenberg_minimum_version = '9.0.0'; // https://github.com/WordPress/gutenberg/releases/tag/v9.0.0.
		$wp_minimum_version        = '5.6';
		$has_gutenberg             = is_plugin_active( 'gutenberg/gutenberg.php' );
		$gutenberg_version         = $has_gutenberg ? get_plugin_data( WP_PLUGIN_DIR . '/gutenberg/gutenberg.php' )['Version'] : false;

		if ( $gutenberg_version && version_compare( $gutenberg_version, $gutenberg_minimum_version, '>=' ) ) {
			return true;
		}

		// Get unmodified $wp_version.
		include ABSPATH . WPINC . '/version.php';

		// Strip '-src' from the version string. Messes up version_compare().
		$wp_version = str_replace( '-src', '', $wp_version );

		if ( version_compare( $wp_version, $wp_minimum_version, '>=' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Reloads the page when the option is toggled to make sure all nav features are loaded.
	 *
	 * @param string $old_value Old value.
	 * @param string $value     New value.
	 */
	public static function reload_page_on_toggle( $old_value, $value ) {
		if ( $old_value === $value ) {
			return;
		}

		if ( 'yes' !== $value ) {
			update_option( 'woocommerce_navigation_show_opt_out', 'yes' );
		}

		self::$is_updated = true;
	}

	/**
	 * Reload the page if the setting has been updated.
	 */
	public static function maybe_reload_page() {
		if ( ! isset( $_SERVER['REQUEST_URI'] ) || ! self::$is_updated ) {
			return;
		}

		wp_safe_redirect( wp_unslash( $_SERVER['REQUEST_URI'] ) );
		exit();
	}

	/**
	 * Enqueue the opt out scripts.
	 */
	public function maybe_enqueue_opt_out_scripts() {
		if ( get_option( 'woocommerce_navigation_show_opt_out', 'no' ) !== 'yes' ) {
			return;
		}

		$rtl = is_rtl() ? '.rtl' : '';
		wp_enqueue_style(
			'wc-admin-navigation-opt-out',
			WCAdminAssets::get_url( "navigation-opt-out/style{$rtl}", 'css' ),
			array( 'wp-components' ),
			WCAdminAssets::get_file_version( 'css' )
		);

		WCAdminAssets::register_script( 'wp-admin-scripts', 'navigation-opt-out', true );
		wp_localize_script(
			'wc-admin-navigation-opt-out',
			'surveyData',
			array(
				'url' => Survey::get_url( '/new-navigation-opt-out' ),
			)
		);
		delete_option( 'woocommerce_navigation_show_opt_out' );
	}
}
Admin/Features/NewProductManagementExperience.php000064400000001556152333011040016161 0ustar00<?php
/**
 * WooCommerce New Product Management Experience
 */

namespace Automattic\WooCommerce\Admin\Features;

use \Automattic\WooCommerce\Admin\PageController;
use \Automattic\WooCommerce\Internal\Admin\Loader;

/**
 * Loads assets related to the new product management experience page.
 */
class NewProductManagementExperience {

	/**
	 * Constructor
	 */
	public function __construct() {
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
	}

	/**
	 * Enqueue styles needed for the rich text editor.
	 */
	public function enqueue_styles() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}
		wp_enqueue_style( 'wp-edit-blocks' );
		wp_enqueue_style( 'wp-format-library' );
		wp_enqueue_editor();
		/**
		 * Enqueue any block editor related assets.
		 *
		 * @since 7.1.0
		*/
		do_action( 'enqueue_block_editor_assets' );
	}

}
Admin/Features/Features.php000064400000027357152333011040011647 0ustar00<?php
/**
 * Features loader for features developed in WooCommerce Admin.
 */

namespace Automattic\WooCommerce\Admin\Features;

use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Internal\Admin\Loader;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Features Class.
 */
class Features {
	/**
	 * Class instance.
	 *
	 * @var Loader instance
	 */
	protected static $instance = null;

	/**
	 * Optional features
	 *
	 * @var array
	 */
	protected static $optional_features = array(
		'multichannel-marketing'     => array( 'default' => 'no' ),
		'navigation'                 => array( 'default' => 'no' ),
		'settings'                   => array( 'default' => 'no' ),
		'analytics'                  => array( 'default' => 'yes' ),
		'remote-inbox-notifications' => array( 'default' => 'yes' ),
	);

	/**
	 * Beta features
	 *
	 * @var array
	 */
	protected static $beta_features = array(
		'multichannel-marketing',
		'navigation',
		'settings',
	);

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->register_internal_class_aliases();
		// Load feature before WooCommerce update hooks.
		add_action( 'init', array( __CLASS__, 'load_features' ), 4 );
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_load_beta_features_modal' ) );
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'load_scripts' ), 15 );
		add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_classes' ) );
		add_filter( 'update_option_woocommerce_allow_tracking', array( __CLASS__, 'maybe_disable_features' ), 10, 2 );
	}

	/**
	 * Gets a build configured array of enabled WooCommerce Admin features/sections, but does not respect optionally disabled features.
	 *
	 * @return array Enabled Woocommerce Admin features/sections.
	 */
	public static function get_features() {
		return apply_filters( 'woocommerce_admin_features', array() );
	}

	/**
	 * Gets the optional feature options as an associative array that can be toggled on or off.
	 *
	 * @return array
	 */
	public static function get_optional_feature_options() {
		$features = [];

		foreach ( array_keys( self::$optional_features ) as $optional_feature_key ) {
			$feature_class = self::get_feature_class( $optional_feature_key );

			if ( $feature_class ) {
				$features[ $optional_feature_key ] = $feature_class::TOGGLE_OPTION_NAME;
			}
		}
		return $features;
	}

	/**
	 * Returns if a specific wc-admin feature exists in the current environment.
	 *
	 * @param  string $feature Feature slug.
	 * @return bool Returns true if the feature exists.
	 */
	public static function exists( $feature ) {
		$features = self::get_features();
		return in_array( $feature, $features, true );
	}

	/**
	 * Get the feature class as a string.
	 *
	 * @param string $feature Feature name.
	 * @return string|null
	 */
	public static function get_feature_class( $feature ) {
		$feature       = str_replace( '-', '', ucwords( strtolower( $feature ), '-' ) );
		$feature_class = 'Automattic\\WooCommerce\\Admin\\Features\\' . $feature;

		if ( class_exists( $feature_class ) ) {
			return $feature_class;
		}

		// Handle features contained in subdirectory.
		if ( class_exists( $feature_class . '\\Init' ) ) {
			return $feature_class . '\\Init';
		}

		return null;
	}

	/**
	 * Class loader for enabled WooCommerce Admin features/sections.
	 */
	public static function load_features() {
		$features = self::get_features();
		foreach ( $features as $feature ) {
			$feature_class = self::get_feature_class( $feature );

			if ( $feature_class ) {
				new $feature_class();
			}
		}
	}

	/**
	 * Gets a build configured array of enabled WooCommerce Admin respecting optionally disabled features.
	 *
	 * @return array Enabled Woocommerce Admin features/sections.
	 */
	public static function get_available_features() {
		$features                      = self::get_features();
		$optional_feature_keys         = array_keys( self::$optional_features );
		$optional_features_unavailable = [];

		/**
		 * Filter allowing WooCommerce Admin optional features to be disabled.
		 *
		 * @param bool $disabled False.
		 */
		if ( apply_filters( 'woocommerce_admin_disabled', false ) ) {
			return array_values( array_diff( $features, $optional_feature_keys ) );
		}

		foreach ( $optional_feature_keys as $optional_feature_key ) {
			$feature_class = self::get_feature_class( $optional_feature_key );

			if ( $feature_class ) {
				$default = isset( self::$optional_features[ $optional_feature_key ]['default'] ) ?
					self::$optional_features[ $optional_feature_key ]['default'] :
					'no';

				// Check if the feature is currently being enabled, if it is continue.
				/* phpcs:disable WordPress.Security.NonceVerification */
				$feature_option = $feature_class::TOGGLE_OPTION_NAME;
				if ( isset( $_POST[ $feature_option ] ) && '1' === $_POST[ $feature_option ] ) {
					continue;
				}

				if ( 'yes' !== get_option( $feature_class::TOGGLE_OPTION_NAME, $default ) ) {
					$optional_features_unavailable[] = $optional_feature_key;
				}
			}
		}

		return array_values( array_diff( $features, $optional_features_unavailable ) );
	}

	/**
	 * Check if a feature is enabled.
	 *
	 * @param string $feature Feature slug.
	 * @return bool
	 */
	public static function is_enabled( $feature ) {
		$available_features = self::get_available_features();
		return in_array( $feature, $available_features, true );
	}

	/**
	 * Enable a toggleable optional feature.
	 *
	 * @param string $feature Feature name.
	 * @return bool
	 */
	public static function enable( $feature ) {
		$features = self::get_optional_feature_options();

		if ( isset( $features[ $feature ] ) ) {
			update_option( $features[ $feature ], 'yes' );
			return true;
		}

		return false;
	}

	/**
	 * Disable a toggleable optional feature.
	 *
	 * @param string $feature Feature name.
	 * @return bool
	 */
	public static function disable( $feature ) {
		$features = self::get_optional_feature_options();

		if ( isset( $features[ $feature ] ) ) {
			update_option( $features[ $feature ], 'no' );
			return true;
		}

		return false;
	}

	/**
	 * Disable features when opting out of tracking.
	 *
	 * @param string $old_value Old value.
	 * @param string $value New value.
	 */
	public static function maybe_disable_features( $old_value, $value ) {
		if ( 'yes' === $value ) {
			return;
		}

		foreach ( self::$beta_features as $feature ) {
			self::disable( $feature );
		}
	}

	/**
	 * Adds the Features section to the advanced tab of WooCommerce Settings
	 *
	 * @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class).
	 *
	 * @param array $sections Sections.
	 * @return array
	 */
	public static function add_features_section( $sections ) {
		return $sections;
	}

	/**
	 * Adds the Features settings.
	 *
	 * @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class).
	 *
	 * @param array  $settings Settings.
	 * @param string $current_section Current section slug.
	 * @return array
	 */
	public static function add_features_settings( $settings, $current_section ) {
		return $settings;
	}

	/**
	 * Conditionally loads the beta features tracking modal.
	 *
	 * @param string $hook Page hook.
	 */
	public static function maybe_load_beta_features_modal( $hook ) {
		if (
			'woocommerce_page_wc-settings' !== $hook ||
			! isset( $_GET['tab'] ) || 'advanced' !== $_GET['tab'] || // phpcs:ignore CSRF ok.
			! isset( $_GET['section'] ) || 'features' !== $_GET['section'] // phpcs:ignore CSRF ok.
		) {
			return;
		}
		$tracking_enabled = get_option( 'woocommerce_allow_tracking', 'no' );

		if ( empty( self::$beta_features ) ) {
			return;
		}

		if ( 'yes' === $tracking_enabled ) {
			return;
		}

		$rtl = is_rtl() ? '.rtl' : '';

		wp_enqueue_style(
			'wc-admin-beta-features-tracking-modal',
			WCAdminAssets::get_url( "beta-features-tracking-modal/style{$rtl}", 'css' ),
			array( 'wp-components' ),
			WCAdminAssets::get_file_version( 'css' )
		);

		wp_enqueue_script(
			'wc-admin-beta-features-tracking-modal',
			WCAdminAssets::get_url( 'wp-admin-scripts/beta-features-tracking-modal', 'js' ),
			array( 'wp-i18n', 'wp-element', WC_ADMIN_APP ),
			WCAdminAssets::get_file_version( 'js' ),
			true
		);
	}

	/**
	 * Loads the required scripts on the correct pages.
	 */
	public static function load_scripts() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}

		$features         = self::get_features();
		$enabled_features = array();
		foreach ( $features as $key ) {
			$enabled_features[ $key ] = self::is_enabled( $key );
		}
		wp_add_inline_script( WC_ADMIN_APP, 'window.wcAdminFeatures = ' . wp_json_encode( $enabled_features ), 'before' );
	}


	/**
	 * Adds body classes to the main wp-admin wrapper, allowing us to better target elements in specific scenarios.
	 *
	 * @param string $admin_body_class Body class to add.
	 */
	public static function add_admin_body_classes( $admin_body_class = '' ) {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return $admin_body_class;
		}

		$classes = explode( ' ', trim( $admin_body_class ) );

		$features = self::get_features();
		foreach ( $features as $feature_key ) {
			$classes[] = sanitize_html_class( 'woocommerce-feature-enabled-' . $feature_key );
		}

		$admin_body_class = implode( ' ', array_unique( $classes ) );
		return " $admin_body_class ";
	}

	/**
	 * Alias internal features classes to make them backward compatible.
	 * We've moved our feature classes to src-internal as part of merging this
	 * repository with WooCommerce Core to form a monorepo.
	 * See https://wp.me/p90Yrv-2HY for details.
	 */
	private function register_internal_class_aliases() {
		$aliases = array(
			// new class => original class (this will be aliased).
			'Automattic\WooCommerce\Internal\Admin\WCPayPromotion\Init' => 'Automattic\WooCommerce\Admin\Features\WcPayPromotion\Init',
			'Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\Init' => 'Automattic\WooCommerce\Admin\Features\RemoteFreeExtensions\Init',
			'Automattic\WooCommerce\Internal\Admin\ActivityPanels' => 'Automattic\WooCommerce\Admin\Features\ActivityPanels',
			'Automattic\WooCommerce\Internal\Admin\Analytics' => 'Automattic\WooCommerce\Admin\Features\Analytics',
			'Automattic\WooCommerce\Internal\Admin\Coupons' => 'Automattic\WooCommerce\Admin\Features\Coupons',
			'Automattic\WooCommerce\Internal\Admin\CouponsMovedTrait' => 'Automattic\WooCommerce\Admin\Features\CouponsMovedTrait',
			'Automattic\WooCommerce\Internal\Admin\CustomerEffortScoreTracks' => 'Automattic\WooCommerce\Admin\Features\CustomerEffortScoreTracks',
			'Automattic\WooCommerce\Internal\Admin\Homescreen' => 'Automattic\WooCommerce\Admin\Features\Homescreen',
			'Automattic\WooCommerce\Internal\Admin\Marketing' => 'Automattic\WooCommerce\Admin\Features\Marketing',
			'Automattic\WooCommerce\Internal\Admin\MobileAppBanner' => 'Automattic\WooCommerce\Admin\Features\MobileAppBanner',
			'Automattic\WooCommerce\Internal\Admin\RemoteInboxNotifications' => 'Automattic\WooCommerce\Admin\Features\RemoteInboxNotifications',
			'Automattic\WooCommerce\Internal\Admin\SettingsNavigationFeature' => 'Automattic\WooCommerce\Admin\Features\Settings',
			'Automattic\WooCommerce\Internal\Admin\ShippingLabelBanner' => 'Automattic\WooCommerce\Admin\Features\ShippingLabelBanner',
			'Automattic\WooCommerce\Internal\Admin\ShippingLabelBannerDisplayRules' => 'Automattic\WooCommerce\Admin\Features\ShippingLabelBannerDisplayRules',
			'Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage' => 'Automattic\WooCommerce\Admin\Features\WcPayWelcomePage',
		);
		foreach ( $aliases as $new_class => $orig_class ) {
			class_alias( $new_class, $orig_class );
		}
	}
}
Admin/Features/OnboardingTasks/TaskTraits.php000064400000001713152333011040015236 0ustar00<?php
/**
 * Task and TaskList Traits
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

defined( 'ABSPATH' ) || exit;

/**
 * TaskTraits class.
 */
trait TaskTraits {
	/**
	 * Record a tracks event with the prefixed event name.
	 *
	 * @param string $event_name Event name.
	 * @param array  $args Array of tracks arguments.
	 * @return string Prefixed event name.
	 */
	public function record_tracks_event( $event_name, $args = array() ) {
		if ( ! $this->get_list_id() ) {
			return;
		}

		$prefixed_event_name = $this->prefix_event( $event_name );

		wc_admin_record_tracks_event(
			$prefixed_event_name,
			$args
		);

		return $prefixed_event_name;
	}

	/**
	 * Get the task list ID.
	 *
	 * @return string
	 */
	public function get_list_id() {
		$namespaced_class = get_class( $this );
		return is_subclass_of( $namespaced_class, 'Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task' )
			? $this->get_parent_id()
			: $this->id;
	}
}
Admin/Features/OnboardingTasks/Task.php000064400000027701152333011040014054 0ustar00<?php
/**
 * Handles task related methods.
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

use Automattic\WooCommerce\Internal\Admin\WCAdminUser;

/**
 * Task class.
 */
abstract class Task {
	/**
	 * Task traits.
	 */
	use TaskTraits;

	/**
	 * Name of the dismiss option.
	 *
	 * @var string
	 */
	const DISMISSED_OPTION = 'woocommerce_task_list_dismissed_tasks';

	/**
	 * Name of the snooze option.
	 *
	 * @var string
	 *
	 * @deprecated 7.2.0
	 */
	const SNOOZED_OPTION = 'woocommerce_task_list_remind_me_later_tasks';

	/**
	 * Name of the actioned option.
	 *
	 * @var string
	 */
	const ACTIONED_OPTION = 'woocommerce_task_list_tracked_completed_actions';

	/**
	 * Option name of completed tasks.
	 *
	 * @var string
	 */
	const COMPLETED_OPTION = 'woocommerce_task_list_tracked_completed_tasks';

	/**
	 * Name of the active task transient.
	 *
	 * @var string
	 */
	const ACTIVE_TASK_TRANSIENT = 'wc_onboarding_active_task';

	/**
	 * Parent task list.
	 *
	 * @var TaskList
	 */
	protected $task_list;

	/**
	 * Duration to milisecond mapping.
	 *
	 * @var string
	 */
	protected $duration_to_ms = array(
		'day'  => DAY_IN_SECONDS * 1000,
		'hour' => HOUR_IN_SECONDS * 1000,
		'week' => WEEK_IN_SECONDS * 1000,
	);

	/**
	 * Constructor
	 *
	 * @param TaskList|null $task_list Parent task list.
	 */
	public function __construct( $task_list = null ) {
		$this->task_list = $task_list;
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	abstract public function get_id();

	/**
	 * Title.
	 *
	 * @return string
	 */
	abstract public function get_title();

	/**
	 * Content.
	 *
	 * @return string
	 */
	abstract public function get_content();

	/**
	 * Time.
	 *
	 * @return string
	 */
	abstract public function get_time();

	/**
	 * Parent ID.
	 *
	 * @return string
	 */
	public function get_parent_id() {
		if ( ! $this->task_list ) {
			return '';
		}
		return $this->task_list->get_list_id();
	}

	/**
	 * Get task list options.
	 *
	 * @return array
	 */
	public function get_parent_options() {
		if ( ! $this->task_list ) {
			return array();
		}
		return $this->task_list->options;
	}

	/**
	 * Get custom option.
	 *
	 * @param string $option_name name of custom option.
	 * @return mixed|null
	 */
	public function get_parent_option( $option_name ) {
		if ( $this->task_list && isset( $this->task_list->options[ $option_name ] ) ) {
			return $this->task_list->options[ $option_name ];
		}
		return null;
	}


	/**
	 * Prefix event for track event naming.
	 *
	 * @param string $event_name Event name.
	 * @return string
	 */
	public function prefix_event( $event_name ) {
		if ( ! $this->task_list ) {
			return '';
		}
		return $this->task_list->prefix_event( $event_name );
	}

	/**
	 * Additional info.
	 *
	 * @return string
	 */
	public function get_additional_info() {
		return '';
	}

	/**
	 * Additional data.
	 *
	 * @return mixed
	 */
	public function get_additional_data() {
		return null;
	}

	/**
	 * Level.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return string
	 */
	public function get_level() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		return 3;
	}

	/**
	 * Action label.
	 *
	 * @return string
	 */
	public function get_action_label() {
		return __( "Let's go", 'woocommerce' );
	}

	/**
	 * Action URL.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return null;
	}

	/**
	 * Check if a task is dismissable.
	 *
	 * @return bool
	 */
	public function is_dismissable() {
		return false;
	}

	/**
	 * Bool for task dismissal.
	 *
	 * @return bool
	 */
	public function is_dismissed() {
		if ( ! $this->is_dismissable() ) {
			return false;
		}

		$dismissed = get_option( self::DISMISSED_OPTION, array() );

		return in_array( $this->get_id(), $dismissed, true );
	}

	/**
	 * Dismiss the task.
	 *
	 * @return bool
	 */
	public function dismiss() {
		if ( ! $this->is_dismissable() ) {
			return false;
		}

		$dismissed   = get_option( self::DISMISSED_OPTION, array() );
		$dismissed[] = $this->get_id();
		$update      = update_option( self::DISMISSED_OPTION, array_unique( $dismissed ) );

		if ( $update ) {
			$this->record_tracks_event( 'dismiss_task', array( 'task_name' => $this->get_id() ) );
		}

		return $update;
	}

	/**
	 * Undo task dismissal.
	 *
	 * @return bool
	 */
	public function undo_dismiss() {
		$dismissed = get_option( self::DISMISSED_OPTION, array() );
		$dismissed = array_diff( $dismissed, array( $this->get_id() ) );
		$update    = update_option( self::DISMISSED_OPTION, $dismissed );

		if ( $update ) {
			$this->record_tracks_event( 'undo_dismiss_task', array( 'task_name' => $this->get_id() ) );
		}

		return $update;
	}

	/**
	 * Check if a task is snoozeable.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return bool
	 */
	public function is_snoozeable() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		return false;
	}

	/**
	 * Get the snoozed until datetime.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return string
	 */
	public function get_snoozed_until() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		$snoozed_tasks = get_option( self::SNOOZED_OPTION, array() );
		if ( isset( $snoozed_tasks[ $this->get_id() ] ) ) {
			return $snoozed_tasks[ $this->get_id() ];
		}

		return null;
	}

	/**
	 * Bool for task snoozed.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return bool
	 */
	public function is_snoozed() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		if ( ! $this->is_snoozeable() ) {
			return false;
		}

		$snoozed = get_option( self::SNOOZED_OPTION, array() );

		return isset( $snoozed[ $this->get_id() ] ) && $snoozed[ $this->get_id() ] > ( time() * 1000 );
	}

	/**
	 * Snooze the task.
	 *
	 * @param string $duration Duration to snooze. day|hour|week.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return bool
	 */
	public function snooze( $duration = 'day' ) {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		if ( ! $this->is_snoozeable() ) {
			return false;
		}

		$snoozed                    = get_option( self::SNOOZED_OPTION, array() );
		$snoozed_until              = $this->duration_to_ms[ $duration ] + ( time() * 1000 );
		$snoozed[ $this->get_id() ] = $snoozed_until;
		$update                     = update_option( self::SNOOZED_OPTION, $snoozed );

		if ( $update ) {
			if ( $update ) {
				$this->record_tracks_event( 'remindmelater_task', array( 'task_name' => $this->get_id() ) );
			}
		}

		return $update;
	}

	/**
	 * Undo task snooze.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return bool
	 */
	public function undo_snooze() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		$snoozed = get_option( self::SNOOZED_OPTION, array() );
		unset( $snoozed[ $this->get_id() ] );
		$update = update_option( self::SNOOZED_OPTION, $snoozed );

		if ( $update ) {
			$this->record_tracks_event( 'undo_remindmelater_task', array( 'task_name' => $this->get_id() ) );
		}

		return $update;
	}

	/**
	 * Check if a task list has previously been marked as complete.
	 *
	 * @return bool
	 */
	public function has_previously_completed() {
		$complete = get_option( self::COMPLETED_OPTION, array() );
		return in_array( $this->get_id(), $complete, true );
	}

	/**
	 * Track task completion if task is viewable.
	 */
	public function possibly_track_completion() {
		if ( ! $this->is_complete() ) {
			return;
		}

		if ( $this->has_previously_completed() ) {
			return;
		}

		$completed_tasks   = get_option( self::COMPLETED_OPTION, array() );
		$completed_tasks[] = $this->get_id();
		update_option( self::COMPLETED_OPTION, $completed_tasks );
		$this->record_tracks_event( 'task_completed', array( 'task_name' => $this->get_id() ) );
	}

	/**
	 * Set this as the active task across page loads.
	 */
	public function set_active() {
		if ( $this->is_complete() ) {
			return;
		}

		set_transient(
			self::ACTIVE_TASK_TRANSIENT,
			$this->get_id(),
			DAY_IN_SECONDS
		);
	}

	/**
	 * Check if this is the active task.
	 */
	public function is_active() {
		return get_transient( self::ACTIVE_TASK_TRANSIENT ) === $this->get_id();
	}

	/**
	 * Check if the store is capable of viewing the task.
	 *
	 * @return bool
	 */
	public function can_view() {
		return true;
	}

	/**
	 * Check if task is disabled.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return bool
	 */
	public function is_disabled() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		return false;
	}

	/**
	 * Check if the task is complete.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return self::is_actioned();
	}

	/**
	 * Check if the task has been visited.
	 *
	 * @return bool
	 */
	public function is_visited() {
		$user_id       = get_current_user_id();
		$response      = WCAdminUser::get_user_data_field( $user_id, 'task_list_tracked_started_tasks' );
		$tracked_tasks = $response ? json_decode( $response, true ) : array();

		return isset( $tracked_tasks[ $this->get_id() ] ) && $tracked_tasks[ $this->get_id() ] > 0;
	}

	/**
	 * Check if should record event when task is viewed
	 *
	 * @return bool
	 */
	public function get_record_view_event(): bool {
		return false;
	}

	/**
	 * Get the task as JSON.
	 *
	 * @return array
	 */
	public function get_json() {
		$this->possibly_track_completion();

		return array(
			'id'              => $this->get_id(),
			'parentId'        => $this->get_parent_id(),
			'title'           => $this->get_title(),
			'canView'         => $this->can_view(),
			'content'         => $this->get_content(),
			'additionalInfo'  => $this->get_additional_info(),
			'actionLabel'     => $this->get_action_label(),
			'actionUrl'       => $this->get_action_url(),
			'isComplete'      => $this->is_complete(),
			'time'            => $this->get_time(),
			'level'           => 3,
			'isActioned'      => $this->is_actioned(),
			'isDismissed'     => $this->is_dismissed(),
			'isDismissable'   => $this->is_dismissable(),
			'isSnoozed'       => false,
			'isSnoozeable'    => false,
			'isVisited'       => $this->is_visited(),
			'isDisabled'      => false,
			'snoozedUntil'    => null,
			'additionalData'  => self::convert_object_to_camelcase( $this->get_additional_data() ),
			'eventPrefix'     => $this->prefix_event( '' ),
			'recordViewEvent' => $this->get_record_view_event(),
		);
	}

	/**
	 * Convert object keys to camelcase.
	 *
	 * @param array $data Data to convert.
	 * @return object
	 */
	public static function convert_object_to_camelcase( $data ) {
		if ( ! is_array( $data ) ) {
			return $data;
		}

		$new_object = (object) array();

		foreach ( $data as $key => $value ) {
			$new_key              = lcfirst( implode( '', array_map( 'ucfirst', explode( '_', $key ) ) ) );
			$new_object->$new_key = $value;
		}

		return $new_object;
	}

	/**
	 * Mark a task as actioned.  Used to verify an action has taken place in some tasks.
	 *
	 * @return bool
	 */
	public function mark_actioned() {
		$actioned = get_option( self::ACTIONED_OPTION, array() );

		$actioned[] = $this->get_id();
		$update     = update_option( self::ACTIONED_OPTION, array_unique( $actioned ) );

		if ( $update ) {
			$this->record_tracks_event( 'actioned_task', array( 'task_name' => $this->get_id() ) );
		}

		return $update;
	}

	/**
	 * Check if a task has been actioned.
	 *
	 * @return bool
	 */
	public function is_actioned() {
		return self::is_task_actioned( $this->get_id() );
	}

	/**
	 * Check if a provided task ID has been actioned.
	 *
	 * @param string $id Task ID.
	 * @return bool
	 */
	public static function is_task_actioned( $id ) {
		$actioned = get_option( self::ACTIONED_OPTION, array() );
		return in_array( $id, $actioned, true );
	}

	/**
	 * Sorting function for tasks.
	 *
	 * @param Task  $a Task a.
	 * @param Task  $b Task b.
	 * @param array $sort_by list of columns with sort order.
	 * @return int
	 */
	public static function sort( $a, $b, $sort_by = array() ) {
		$result = 0;
		foreach ( $sort_by as $data ) {
			$key   = $data['key'];
			$a_val = $a->$key ?? false;
			$b_val = $b->$key ?? false;
			if ( 'asc' === $data['order'] ) {
				$result = $a_val <=> $b_val;
			} else {
				$result = $b_val <=> $a_val;
			}

			if ( 0 !== $result ) {
				break;
			}
		}
		return $result;
	}

}
Admin/Features/OnboardingTasks/DeprecatedExtendedTask.php000064400000005252152333011040017513 0ustar00<?php
/**
 * A temporary class for creating tasks on the fly from deprecated tasks.
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

/**
 * DeprecatedExtendedTask class.
 */
class DeprecatedExtendedTask extends Task {
	/**
	 * ID.
	 *
	 * @var string
	 */
	public $id = '';

	/**
	 * Snoozeable.
	 *
	 * @var boolean
	 */
	public $is_snoozeable = false;

	/**
	 * Dismissable.
	 *
	 * @var boolean
	 */
	public $is_dismissable = false;

	/**
	 * Constructor.
	 *
	 * @param TaskList $task_list Parent task list.
	 * @param array    $args Array of task args.
	 */
	public function __construct( $task_list, $args ) {
		parent::__construct( $task_list );
		$task_args = wp_parse_args(
			$args,
			array(
				'id'              => null,
				'is_dismissable'  => false,
				'is_snoozeable'   => false,
				'can_view'        => true,
				'level'           => 3,
				'additional_info' => null,
				'content'         => '',
				'title'           => '',
				'is_complete'     => false,
				'time'            => null,
			)
		);

		$this->id              = $task_args['id'];
		$this->additional_info = $task_args['additional_info'];
		$this->content         = $task_args['content'];
		$this->is_complete     = $task_args['is_complete'];
		$this->is_dismissable  = $task_args['is_dismissable'];
		$this->is_snoozeable   = $task_args['is_snoozeable'];
		$this->can_view        = $task_args['can_view'];
		$this->level           = $task_args['level'];
		$this->time            = $task_args['time'];
		$this->title           = $task_args['title'];
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return $this->id;
	}

	/**
	 * Additonal info.
	 *
	 * @return string
	 */
	public function get_additional_info() {
		return $this->additional_info;
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return $this->content;
	}

	/**
	 * Level.
	 *
	 * @return int
	 */
	public function get_level() {
		return $this->level;
	}

	/**
	 * Title
	 *
	 * @return string
	 */
	public function get_title() {
		return $this->title;
	}

	/**
	 * Time
	 *
	 * @return string|null
	 */
	public function get_time() {
		return $this->time;
	}

	/**
	 * Check if a task is snoozeable.
	 *
	 * @return bool
	 */
	public function is_snoozeable() {
		return $this->is_snoozeable;
	}

	/**
	 * Check if a task is dismissable.
	 *
	 * @return bool
	 */
	public function is_dismissable() {
		return $this->is_dismissable;
	}

	/**
	 * Check if a task is dismissable.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return $this->is_complete;
	}

	/**
	 * Check if a task is dismissable.
	 *
	 * @return bool
	 */
	public function can_view() {
		return $this->can_view;
	}
}
Admin/Features/OnboardingTasks/Tasks/Payments.php000064400000003746152333011040016042 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;

/**
 * Payments Task
 */
class Payments extends Task {

	/**
	 * Used to cache is_complete() method result.
	 * @var null
	 */
	private $is_complete_result = null;

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'payments';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( true === $this->get_parent_option( 'use_completed_title' ) ) {
			if ( $this->is_complete() ) {
				return __( 'You set up payments', 'woocommerce' );
			}
			return __( 'Set up payments', 'woocommerce' );
		}
		return __( 'Set up payments', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			'Choose payment providers and enable payment methods at checkout.',
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '2 minutes', 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		if ( $this->is_complete_result === null ) {
			$this->is_complete_result = self::has_gateways();
		}

		return $this->is_complete_result;
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		$woocommerce_payments = $this->task_list->get_task( 'woocommerce-payments' );
		return Features::is_enabled( 'payment-gateway-suggestions' ) && ! $woocommerce_payments->can_view();
	}

	/**
	 * Check if the store has any enabled gateways.
	 *
	 * @return bool
	 */
	public static function has_gateways() {
		$gateways         = WC()->payment_gateways->get_available_payment_gateways();
		$enabled_gateways = array_filter(
			$gateways,
			function( $gateway ) {
				return 'yes' === $gateway->enabled && 'woocommerce_payments' !== $gateway->id;
			}
		);

		return ! empty( $enabled_gateways );
	}
}
Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php000064400000004513152333011040020024 0ustar00<?php


namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Payments;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments;

/**
 * Payments Task
 */
class AdditionalPayments extends Payments {

	/**
	 * Used to cache is_complete() method result.
	 * @var null
	 */
	private $is_complete_result = null;


	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'payments';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __(
			'Set up additional payment options',
			'woocommerce'
		);
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			'Choose payment providers and enable payment methods at checkout.',
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '2 minutes', 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		if ( $this->is_complete_result === null ) {
			$this->is_complete_result = self::has_gateways();
		}

		return $this->is_complete_result;
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		if ( ! Features::is_enabled( 'payment-gateway-suggestions' ) ) {
			// Hide task if feature not enabled.
			return false;
		}

		$woocommerce_payments = new WooCommercePayments();

		if ( ! $woocommerce_payments->is_requested() || ! $woocommerce_payments->is_supported() || ! $woocommerce_payments->is_connected() ) {
			// Hide task if WC Pay is not installed via OBW, or is not connected, or the store is located in a country that is not supported by WC Pay.
			return false;
		}
		if ( $this->get_parent_id() === 'extended_two_column' && WooCommercePayments::is_connected() ) {
			return false;
		}

		return true;
	}

	/**
	 * Check if the store has any enabled gateways.
	 *
	 * @return bool
	 */
	public static function has_gateways() {
		$gateways         = WC()->payment_gateways->get_available_payment_gateways();
		$enabled_gateways = array_filter(
			$gateways,
			function( $gateway ) {
				return 'yes' === $gateway->enabled && 'woocommerce_payments' !== $gateway->id;
			}
		);

		return ! empty( $enabled_gateways );
	}
}
Admin/Features/OnboardingTasks/Tasks/GetMobileApp.php000064400000005012152333011040016536 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\Jetpack\Connection\Manager; // https://github.com/Automattic/jetpack/blob/trunk/projects/packages/connection/src/class-manager.php .

/**
 * Get Mobile App Task
 */
class GetMobileApp extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'get-mobile-app';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __( 'Get the free WooCommerce mobile app', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return '';
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return get_option( 'woocommerce_admin_dismissed_mobile_app_modal' ) === 'yes';
	}

	/**
	 * Task visibility.
	 * Can view under these conditions:
	 *  - Jetpack is installed and connected && current site user has a wordpress.com account connected to jetpack
	 *  - Jetpack is not connected && current user is capable of installing plugins
	 *
	 * @return bool
	 */
	public function can_view() {
		$jetpack_can_be_installed                        = current_user_can( 'manage_woocommerce' ) && current_user_can( 'install_plugins' ) && ! self::is_jetpack_connected();
		$jetpack_is_installed_and_current_user_connected = self::is_current_user_connected();

		return $jetpack_can_be_installed || $jetpack_is_installed_and_current_user_connected;
	}

	/**
	 * Determines if site has any users connected to WordPress.com via JetPack
	 *
	 * @return bool
	 */
	private static function is_jetpack_connected() {
		if ( class_exists( '\Automattic\Jetpack\Connection\Manager' ) && method_exists( '\Automattic\Jetpack\Connection\Manager', 'is_active' ) ) {
			$connection = new Manager();
			return $connection->is_active();
		}
		return false;
	}

	/**
	 * Determines if the current user is connected to Jetpack.
	 *
	 * @return bool
	 */
	private static function is_current_user_connected() {
		if ( class_exists( '\Automattic\Jetpack\Connection\Manager' ) && method_exists( '\Automattic\Jetpack\Connection\Manager', 'is_user_connected' ) ) {
			$connection = new Manager();
			return $connection->is_user_connected();
		}
		return false;
	}

	/**
	 * Action URL.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return admin_url( 'admin.php?page=wc-admin&mobileAppModal=true' );
	}
}
Admin/Features/OnboardingTasks/Tasks/Appearance.php000064400000005544152333011040016277 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Internal\Admin\Loader;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Products;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Appearance Task
 */
class Appearance extends Task {

	/**
	 * Constructor
	 *
	 * @param TaskList $task_list Parent task list.
	 */
	public function __construct( $task_list ) {
		parent::__construct( $task_list );
		add_action( 'admin_enqueue_scripts', array( $this, 'add_media_scripts' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_return_notice_script' ) );
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'appearance';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( $this->get_parent_option( 'use_completed_title' ) === true ) {
			if ( $this->is_complete() ) {
				return __( 'You personalized your store', 'woocommerce' );
			}
			return __( 'Personalize your store', 'woocommerce' );
		}
		return __( 'Personalize my store', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			'Add your logo, create a homepage, and start designing your store.',
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '2 minutes', 'woocommerce' );
	}

	/**
	 * Addtional data.
	 *
	 * @return array
	 */
	public function get_additional_data() {
		return array(
			'has_homepage' => self::has_homepage(),
			'has_products' => Products::has_products(),
			'stylesheet'   => get_option( 'stylesheet' ),
			'theme_mods'   => get_theme_mods(),
		);
	}

	/**
	 * Add media scripts for image uploader.
	 */
	public function add_media_scripts() {
		if ( ! PageController::is_admin_page() || ! $this->can_view() ) {
			return;
		}

		wp_enqueue_media();
	}


	/**
	 * Adds a return to task list notice when completing the task.
	 *
	 * @param string $hook Page hook.
	 */
	public function possibly_add_return_notice_script( $hook ) {
		global $post;

		if ( $hook !== 'post.php' || $post->post_type !== 'page' ) {
			return;
		}

		if ( $this->is_complete() || ! $this->is_active() ) {
			return;
		}

		WCAdminAssets::register_script( 'wp-admin-scripts', 'onboarding-homepage-notice', true );
	}

	/**
	 * Check if the site has a homepage set up.
	 */
	public static function has_homepage() {
		if ( get_option( 'classic-editor-replace' ) === 'classic' ) {
			return true;
		}

		$homepage_id = get_option( 'woocommerce_onboarding_homepage_post_id', false );

		if ( ! $homepage_id ) {
			return false;
		}

		$post      = get_post( $homepage_id );
		$completed = $post && $post->post_status === 'publish';

		return $completed;
	}
}
Admin/Features/OnboardingTasks/Tasks/Products.php000064400000007634152333011040016045 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Products Task
 */
class Products extends Task {

	/**
	 * Constructor
	 *
	 * @param TaskList $task_list Parent task list.
	 */
	public function __construct( $task_list ) {
		parent::__construct( $task_list );
		add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_manual_return_notice_script' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_import_return_notice_script' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_load_sample_return_notice_script' ) );
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'products';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( $this->get_parent_option( 'use_completed_title' ) === true ) {
			if ( $this->is_complete() ) {
				return __( 'You added products', 'woocommerce' );
			}
			return __( 'Add products', 'woocommerce' );
		}
		return __( 'Add my products', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			'Start by adding the first product to your store. You can add your products manually, via CSV, or import them from another service.',
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '1 minute per product', 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return self::has_products();
	}

	/**
	 * Addtional data.
	 *
	 * @return array
	 */
	public function get_additional_data() {
		return array(
			'has_products' => self::has_products(),
		);
	}


	/**
	 * Adds a return to task list notice when completing the manual product task.
	 *
	 * @param string $hook Page hook.
	 */
	public function possibly_add_manual_return_notice_script( $hook ) {
		global $post;
		if ( $hook !== 'post.php' || $post->post_type !== 'product' ) {
			return;
		}

		if ( ! $this->is_active() || ! $this->is_complete() ) {
			return;
		}

		WCAdminAssets::register_script( 'wp-admin-scripts', 'onboarding-product-notice', true );

		// Clear the active task transient to only show notice once per active session.
		delete_transient( self::ACTIVE_TASK_TRANSIENT );
	}

	/**
	 * Adds a return to task list notice when completing the import product task.
	 *
	 * @param string $hook Page hook.
	 */
	public function possibly_add_import_return_notice_script( $hook ) {
		$step = isset( $_GET['step'] ) ? $_GET['step'] : ''; // phpcs:ignore csrf ok, sanitization ok.

		if ( $hook !== 'product_page_product_importer' || $step !== 'done' ) {
			return;
		}

		if ( ! $this->is_active() || $this->is_complete() ) {
			return;
		}

		WCAdminAssets::register_script( 'wp-admin-scripts', 'onboarding-product-import-notice', true );
	}

	/**
	 * Adds a return to task list notice when completing the loading sample products action.
	 *
	 * @param string $hook Page hook.
	 */
	public function possibly_add_load_sample_return_notice_script( $hook ) {
		if ( $hook !== 'edit.php' || get_query_var( 'post_type' ) !== 'product' ) {
			return;
		}

		$referer = wp_get_referer();
		if ( ! $referer || strpos( $referer, wc_admin_url() ) !== 0 ) {
			return;
		}

		if ( ! isset( $_GET[ Task::ACTIVE_TASK_TRANSIENT ] ) ) {
			return;
		}

		$task_id = sanitize_title_with_dashes( wp_unslash( $_GET[ Task::ACTIVE_TASK_TRANSIENT ] ) );
		if ( $task_id !== $this->get_id() || ! $this->is_complete() ) {
			return;
		}

		WCAdminAssets::register_script( 'wp-admin-scripts', 'onboarding-load-sample-products-notice', true );
	}

	/**
	 * Check if the store has any published products.
	 *
	 * @return bool
	 */
	public static function has_products() {
		$counts = wp_count_posts('product');
		return isset( $counts->publish ) && $counts->publish > 0;
	}
}
Admin/Features/OnboardingTasks/Tasks/ReviewShippingOptions.php000064400000002177152333011040020556 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;

/**
 * Review Shipping Options Task
 */
class ReviewShippingOptions extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'review-shipping';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __( 'Review shipping options', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return '';
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return get_option( 'woocommerce_admin_reviewed_default_shipping_zones' ) === 'yes';
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		return get_option( 'woocommerce_admin_created_default_shipping_zones' ) === 'yes';
	}

	/**
	 * Action URL.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return admin_url( 'admin.php?page=wc-settings&tab=shipping' );
	}
}
Admin/Features/OnboardingTasks/Tasks/Marketing.php000064400000004736152333011040016163 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\Init as RemoteFreeExtensions;

/**
 * Marketing Task
 */
class Marketing extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'marketing';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( true === $this->get_parent_option( 'use_completed_title' ) ) {
			if ( $this->is_complete() ) {
				return __( 'You added sales channels', 'woocommerce' );
			}
			return __( 'Get more sales', 'woocommerce' );
		}
		return __( 'Set up marketing tools', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			'Add recommended marketing tools to reach new customers and grow your business',
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '2 minutes', 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return self::has_installed_extensions();
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		return Features::is_enabled( 'remote-free-extensions' ) && count( self::get_plugins() ) > 0;
	}

	/**
	 * Get the marketing plugins.
	 *
	 * @return array
	 */
	public static function get_plugins() {
		$bundles = RemoteFreeExtensions::get_extensions(
			array(
				'task-list/reach',
				'task-list/grow',
			)
		);

		return array_reduce(
			$bundles,
			function( $plugins, $bundle ) {
				$visible = array();
				foreach ( $bundle['plugins'] as $plugin ) {
					if ( $plugin->is_visible ) {
						$visible[] = $plugin;
					}
				}
				return array_merge( $plugins, $visible );
			},
			array()
		);
	}

	/**
	 * Check if the store has installed marketing extensions.
	 *
	 * @return bool
	 */
	public static function has_installed_extensions() {
		$plugins   = self::get_plugins();
		$remaining = array();
		$installed = array();

		foreach ( $plugins as $plugin ) {
			if ( ! $plugin->is_installed ) {
				$remaining[] = $plugin;
			} else {
				$installed[] = $plugin;
			}
		}

		// Make sure the task has been actioned and a marketing extension has been installed.
		if ( count( $installed ) > 0 && Task::is_task_actioned( 'marketing' ) ) {
			return true;
		}

		return false;
	}
}
Admin/Features/OnboardingTasks/Tasks/Purchase.php000064400000012127152333011040016005 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProducts;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingThemes;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;

/**
 * Purchase Task
 */
class Purchase extends Task {
	/**
	 * Constructor
	 *
	 * @param TaskList $task_list Parent task list.
	 */
	public function __construct( $task_list ) {
		parent::__construct( $task_list );
		add_action( 'update_option_woocommerce_onboarding_profile', array( $this, 'clear_dismissal' ), 10, 2 );
	}

	/**
	 * Clear dismissal on onboarding product type changes.
	 *
	 * @param array $old_value Old value.
	 * @param array $new_value New value.
	 */
	public function clear_dismissal( $old_value, $new_value ) {
		$product_types          = isset( $new_value['product_types'] ) ? (array) $new_value['product_types'] : array();
		$previous_product_types = isset( $old_value['product_types'] ) ? (array) $old_value['product_types'] : array();

		if ( empty( array_diff( $product_types, $previous_product_types ) ) ) {
			return;
		}

		$this->undo_dismiss();
	}

	/**
	 * Get the task arguments.
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'purchase';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		$products   = $this->get_paid_products_and_themes();
		$first_product    = count( $products['purchaseable'] ) >= 1 ? $products['purchaseable'][0] : false;

		if ( ! $first_product ) {
			return null;
		}

		$product_label    = isset( $first_product['label'] ) ? $first_product['label'] : $first_product['title'];
		$additional_count = count( $products['purchaseable'] ) - 1;

		if ( $this->get_parent_option( 'use_completed_title' ) && $this->is_complete() ) {
			return count( $products['purchaseable'] ) === 1
				? sprintf(
					/* translators: %1$s: a purchased product name */
					__(
						'You added %1$s',
						'woocommerce'
					),
					$product_label
				)
				: sprintf(
					/* translators: %1$s: a purchased product name, %2$d the number of other products purchased */
					_n(
						'You added %1$s and %2$d other product',
						'You added %1$s and %2$d other products',
						$additional_count,
						'woocommerce'
					),
					$product_label,
					$additional_count
				);
		}

		return count( $products['purchaseable'] ) === 1
			? sprintf(
				/* translators: %1$s: a purchaseable product name */
				__(
					'Add %s to my store',
					'woocommerce'
				),
				$product_label
			)
			: sprintf(
				/* translators: %1$s: a purchaseable product name, %2$d the number of other products to purchase */
				_n(
					'Add %1$s and %2$d more product to my store',
					'Add %1$s and %2$d more products to my store',
					$additional_count,
					'woocommerce'
				),
				$product_label,
				$additional_count
			);
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		$products = $this->get_paid_products_and_themes();

		if ( count( $products['remaining'] ) === 1 ) {
			return isset( $products['purchaseable'][0]['description'] ) ? $products['purchaseable'][0]['description'] : $products['purchaseable'][0]['excerpt'];
		}
		return sprintf(
		/* translators: %1$s: list of product names comma separated, %2%s the last product name */
			__(
				'Good choice! You chose to add %1$s and %2$s to your store.',
				'woocommerce'
			),
			implode( ', ', array_slice( $products['remaining'], 0, -1 ) ) . ( count( $products['remaining'] ) > 2 ? ',' : '' ),
			end( $products['remaining'] )
		);
	}

	/**
	 * Action label.
	 *
	 * @return string
	 */
	public function get_action_label() {
		return __( 'Purchase & install now', 'woocommerce' );
	}


	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '2 minutes', 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		$products = $this->get_paid_products_and_themes();
		return count( $products['remaining'] ) === 0;
	}

	/**
	 * Dismissable.
	 *
	 * @return bool
	 */
	public function is_dismissable() {
		return true;
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		$products = $this->get_paid_products_and_themes();
		return count( $products['purchaseable'] ) > 0;
	}

	/**
	 * Get purchaseable and remaining products.
	 *
	 * @return array purchaseable and remaining products and themes.
	 */
	public static function get_paid_products_and_themes() {
		$relevant_products = OnboardingProducts::get_relevant_products();

		$profiler_data = get_option( OnboardingProfile::DATA_OPTION, array() );
		$theme         = isset( $profiler_data['theme'] ) ? $profiler_data['theme'] : null;
		$paid_theme    = $theme ? OnboardingThemes::get_paid_theme_by_slug( $theme ) : null;
		if ( $paid_theme ) {

			$relevant_products['purchaseable'][] = $paid_theme;

			if ( isset( $paid_theme['is_installed'] ) && false === $paid_theme['is_installed'] ) {
				$relevant_products['remaining'][] = $paid_theme['title'];
			}
		}
		return $relevant_products;
	}
}
Admin/Features/OnboardingTasks/Tasks/Tax.php000064400000007436152333011040014776 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\DataStore as TaxDataStore;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\PluginsHelper;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Tax Task
 */
class Tax extends Task {

	/**
	 * Used to cache is_complete() method result.
	 * @var null
	 */
	private $is_complete_result = null;

	/**
	 * Constructor
	 *
	 * @param TaskList $task_list Parent task list.
	 */
	public function __construct( $task_list ) {
		parent::__construct( $task_list );
		add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_return_notice_script' ) );
	}

	/**
	 * Adds a return to task list notice when completing the task.
	 */
	public function possibly_add_return_notice_script() {
		$page = isset( $_GET['page'] ) ? $_GET['page'] : ''; // phpcs:ignore csrf ok, sanitization ok.
		$tab  = isset( $_GET['tab'] ) ? $_GET['tab'] : ''; // phpcs:ignore csrf ok, sanitization ok.

		if ( $page !== 'wc-settings' || $tab !== 'tax' ) {
			return;
		}

		if ( ! $this->is_active() || $this->is_complete() ) {
			return;
		}

		WCAdminAssets::register_script( 'wp-admin-scripts', 'onboarding-tax-notice', true );
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'tax';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( $this->get_parent_option( 'use_completed_title' ) === true ) {
			if ( $this->is_complete() ) {
				return __( 'You added tax rates', 'woocommerce' );
			}
			return __( 'Add tax rates', 'woocommerce' );
		}
		return __( 'Set up tax rates', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return self::can_use_automated_taxes()
			? __(
				'Good news! WooCommerce Services and Jetpack can automate your sales tax calculations for you.',
				'woocommerce'
			)
			: __(
				'Set your store location and configure tax rate settings.',
				'woocommerce'
			);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '1 minute', 'woocommerce' );
	}

	/**
	 * Action label.
	 *
	 * @return string
	 */
	public function get_action_label() {
		return self::can_use_automated_taxes()
			? __( 'Yes please', 'woocommerce' )
			: __( "Let's go", 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		if ( $this->is_complete_result === null ) {
			$this->is_complete_result = get_option( 'wc_connect_taxes_enabled' ) ||
				count( TaxDataStore::get_taxes( array() ) ) > 0 ||
				get_option( 'woocommerce_no_sales_tax' ) !== false;
		}

		return $this->is_complete_result;
	}

	/**
	 * Addtional data.
	 *
	 * @return array
	 */
	public function get_additional_data() {
		return array(
			'avalara_activated'         => PluginsHelper::is_plugin_active( 'woocommerce-avatax' ),
			'tax_jar_activated'         => class_exists( 'WC_Taxjar' ),
			'woocommerce_tax_countries' => self::get_automated_support_countries(),
		);
	}

	/**
	 * Check if the store has any enabled gateways.
	 *
	 * @return bool
	 */
	public static function can_use_automated_taxes() {
		if ( ! class_exists( 'WC_Taxjar' ) ) {
			return false;
		}

		return in_array( WC()->countries->get_base_country(), self::get_automated_support_countries(), true );
	}

	/**
	 * Get an array of countries that support automated tax.
	 *
	 * @return array
	 */
	public static function get_automated_support_countries() {
		// https://developers.taxjar.com/api/reference/#countries .
		$tax_supported_countries = array_merge(
			array( 'US', 'CA', 'AU', 'GB' ),
			WC()->countries->get_european_union_countries()
		);

		return $tax_supported_countries;
	}
}
Admin/Features/OnboardingTasks/Tasks/TourInAppMarketplace.php000064400000002175152333011040020267 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;

/**
 * Tour In-App Marketplace task
 */
class TourInAppMarketplace extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'tour-in-app-marketplace';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __( 'Tour the WooCommerce Marketplace', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return '';
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return get_option( 'woocommerce_admin_dismissed_in_app_marketplace_tour' ) === 'yes';
	}

	/**
	 * Action URL.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return admin_url( 'admin.php?page=wc-addons&tutorial=true' );
	}

	/**
	 * Check if should record event when task is viewed
	 *
	 * @return bool
	 */
	public function get_record_view_event(): bool {
		return true;
	}
}
Admin/Features/OnboardingTasks/Tasks/ExperimentalShippingRecommendation.php000064400000003216152333011040023256 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\PluginsHelper;

/**
 * Shipping Task
 */
class ExperimentalShippingRecommendation extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'shipping-recommendation';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __( 'Set up shipping', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return '';
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return self::has_plugins_active() && self::has_jetpack_connected();
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		return Features::is_enabled( 'shipping-smart-defaults' );
	}

	/**
	 * Action URL.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return '';
	}

	/**
	 * Check if the store has any shipping zones.
	 *
	 * @return bool
	 */
	public static function has_plugins_active() {
		return PluginsHelper::is_plugin_active( 'woocommerce-services' ) &&
		PluginsHelper::is_plugin_active( 'jetpack' );
	}

	/**
	 * Check if the Jetpack is connected.
	 *
	 * @return bool
	 */
	public static function has_jetpack_connected() {
		if ( class_exists( '\Jetpack' ) && is_callable( '\Jetpack::is_connection_ready' ) ) {
			return \Jetpack::is_connection_ready();
		}
		return false;
	}
}
Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php000064400000007651152333011040020201 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\PluginsHelper;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\Init as Suggestions;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskList;

/**
 * WooCommercePayments Task
 */
class WooCommercePayments extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'woocommerce-payments';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __( 'Set up WooCommerce Payments', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			"You're only one step away from getting paid. Verify your business details to start managing transactions with WooCommerce Payments.",
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '2 minutes', 'woocommerce' );
	}

	/**
	 * Action label.
	 *
	 * @return string
	 */
	public function get_action_label() {
		return __( 'Finish setup', 'woocommerce' );
	}

	/**
	 * Additional info.
	 *
	 * @return string
	 */
	public function get_additional_info() {
		return __(
			'By using WooCommerce Payments you agree to be bound by our <a href="https://wordpress.com/tos/" target="_blank">Terms of Service</a> and acknowledge that you have read our <a href="https://automattic.com/privacy/" target="_blank">Privacy Policy</a>',
			'woocommerce'
		);
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return self::is_connected();
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		$payments = $this->task_list->get_task( 'payments' );

		return ! $payments->is_complete() && // Do not re-display the task if the "add payments" task has already been completed.
			self::is_installed() &&
			self::is_supported() &&
			( $this->get_parent_id() !== 'setup_two_column' || ! self::is_connected() );
	}

	/**
	 * Check if the plugin was requested during onboarding.
	 *
	 * @return bool
	 */
	public static function is_requested() {
		$profiler_data       = get_option( OnboardingProfile::DATA_OPTION, array() );
		$product_types       = isset( $profiler_data['product_types'] ) ? $profiler_data['product_types'] : array();
		$business_extensions = isset( $profiler_data['business_extensions'] ) ? $profiler_data['business_extensions'] : array();

		$subscriptions_and_us = in_array( 'subscriptions', $product_types, true ) && 'US' === WC()->countries->get_base_country();
		return in_array( 'woocommerce-payments', $business_extensions, true ) || $subscriptions_and_us;
	}

	/**
	 * Check if the plugin is installed.
	 *
	 * @return bool
	 */
	public static function is_installed() {
		$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
		return in_array( 'woocommerce-payments', $installed_plugins, true );
	}

	/**
	 * Check if WooCommerce Payments is connected.
	 *
	 * @return bool
	 */
	public static function is_connected() {
		if ( class_exists( '\WC_Payments' ) ) {
			$wc_payments_gateway = \WC_Payments::get_gateway();
			return method_exists( $wc_payments_gateway, 'is_connected' )
				? $wc_payments_gateway->is_connected()
				: false;
		}

		return false;
	}

	/**
	 * Check if the store is in a supported country.
	 *
	 * @return bool
	 */
	public static function is_supported() {
		$suggestions              = Suggestions::get_suggestions();
		$suggestion_plugins       = array_merge(
			...array_filter(
				array_column( $suggestions, 'plugins' ),
				function( $plugins ) {
					return is_array( $plugins );
				}
			)
		);
		$woocommerce_payments_ids = array_search( 'woocommerce-payments', $suggestion_plugins, true );
		if ( false !== $woocommerce_payments_ids ) {
			return true;
		}
		return false;
	}
}
Admin/Features/OnboardingTasks/Tasks/StoreCreation.php000064400000002044152333011040017011 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\Onboarding;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;

/**
 * Store Details Task
 */
class StoreCreation extends Task {

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'store_creation';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		/* translators: Store name */
		return sprintf( __( 'You created %s', 'woocommerce' ), get_bloginfo( 'name' ) );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return '';
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return true;
	}

	/**
	 * Check if task is disabled.
	 *
	 * @return bool
	 */
	public function is_disabled() {
		return true;
	}
}

Admin/Features/OnboardingTasks/Tasks/StoreDetails.php000064400000003267152333011040016642 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;

/**
 * Store Details Task
 */
class StoreDetails extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'store_details';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( true === $this->get_parent_option( 'use_completed_title' ) ) {
			if ( $this->is_complete() ) {
				return __( 'You added store details', 'woocommerce' );
			}
			return __( 'Add store details', 'woocommerce' );
		}
		return __( 'Store details', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			'Your store address is required to set the origin country for shipping, currencies, and payment options.',
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '4 minutes', 'woocommerce' );
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return ! $this->is_complete() ? admin_url( 'admin.php?page=wc-settings&tab=general&tutorial=true' ) : admin_url( 'admin.php?page=wc-settings&tab=general' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		// Mark as completed if the store address, city and postcode are set. We don't need to check the country because it's set by default.
		return get_option( 'woocommerce_store_address', '' ) !== '' && get_option( 'woocommerce_store_city', '' ) !== '' &&
		get_option( 'woocommerce_store_postcode', '' ) !== '';
	}
}
Admin/Features/OnboardingTasks/Tasks/Shipping.php000064400000011545152333011040016017 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use WC_Data_Store;

/**
 * Shipping Task
 */
class Shipping extends Task {

	const ZONE_COUNT_TRANSIENT_NAME = 'woocommerce_shipping_task_zone_count_transient';

	/**
	 * Constructor
	 *
	 * @param TaskList $task_list Parent task list.
	 */
	public function __construct( $task_list = null ) {
		parent::__construct( $task_list );
		// wp_ajax_woocommerce_shipping_zone_methods_save_changes
		// and wp_ajax_woocommerce_shipping_zones_save_changes get fired
		// when a new zone is added or an existing one has been changed.
		add_action( 'wp_ajax_woocommerce_shipping_zones_save_changes', array( __CLASS__, 'delete_zone_count_transient' ), 9 );
		add_action( 'wp_ajax_woocommerce_shipping_zone_methods_save_changes', array( __CLASS__, 'delete_zone_count_transient' ), 9 );
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'shipping';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( true === $this->get_parent_option( 'use_completed_title' ) ) {
			if ( $this->is_complete() ) {
				return __( 'You added shipping costs', 'woocommerce' );
			}
			return __( 'Add shipping costs', 'woocommerce' );
		}
		return __( 'Set up shipping', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			"Set your store location and where you'll ship to.",
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '1 minute', 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return self::has_shipping_zones();
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		if ( Features::is_enabled( 'shipping-smart-defaults' ) ) {
			if ( 'yes' === get_option( 'woocommerce_admin_created_default_shipping_zones' ) ) {
				// If the user has already created a default shipping zone, we don't need to show the task.
				return false;
			}

			/**
			 * Do not display the task when:
			 * - The store sells digital products only
			 * Display the task when:
			 * - We don't know where the store's located
			 * - The store is located in the UK, Australia or Canada
			*/

			if ( self::is_selling_digital_type_only() ) {
				return false;
			}

			$default_store_country = wc_format_country_state_string( get_option( 'woocommerce_default_country', '' ) )['country'];

			// Check if a store address is set so that we don't default to WooCommerce's default country US.
			// Similar logic: https://github.com/woocommerce/woocommerce/blob/059d542394b48468587f252dcb6941c6425cd8d3/plugins/woocommerce-admin/client/profile-wizard/steps/store-details/index.js#L511-L516.
			$store_country = '';
			if ( ! empty( get_option( 'woocommerce_store_address', '' ) ) || 'US' !== $default_store_country ) {
				$store_country = $default_store_country;
			}

			// Unknown country.
			if ( empty( $store_country ) ) {
				return true;
			}

			return in_array( $store_country, array( 'AU', 'CA', 'GB' ), true );
		}

		return self::has_physical_products();
	}

	/**
	 * Action URL.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return self::has_shipping_zones()
			? admin_url( 'admin.php?page=wc-settings&tab=shipping' )
			: null;
	}

	/**
	 * Check if the store has any shipping zones.
	 *
	 * @return bool
	 */
	public static function has_shipping_zones() {
		$zone_count = get_transient( self::ZONE_COUNT_TRANSIENT_NAME );
		if ( false !== $zone_count ) {
			return (int) $zone_count > 0;
		}

		$zone_count = count( WC_Data_Store::load( 'shipping-zone' )->get_zones() );
		set_transient( self::ZONE_COUNT_TRANSIENT_NAME, $zone_count );

		return $zone_count > 0;
	}

	/**
	 * Check if the store has physical products.
	 *
	 * @return bool
	 */
	public static function has_physical_products() {
		$profiler_data = get_option( OnboardingProfile::DATA_OPTION, array() );
		$product_types = isset( $profiler_data['product_types'] ) ? $profiler_data['product_types'] : array();

		return in_array( 'physical', $product_types, true );
	}

	/**
	 * Delete the zone count transient used in has_shipping_zones() method
	 * to refresh the cache.
	 */
	public static function delete_zone_count_transient() {
		delete_transient( self::ZONE_COUNT_TRANSIENT_NAME );
	}

	/**
	 * Check if the store sells digital products only.
	 *
	 * @return bool
	 */
	private static function is_selling_digital_type_only() {
		$profiler_data = get_option( OnboardingProfile::DATA_OPTION, array() );
		$product_types = isset( $profiler_data['product_types'] ) ? $profiler_data['product_types'] : array();

		return array( 'downloads' ) === $product_types;
	}
}
Admin/Features/OnboardingTasks/DeprecatedOptions.php000064400000005000152333011040016552 0ustar00<?php
/**
 * Filters for maintaining backwards compatibility with deprecated options.
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\TaskList;
use WC_Install;

/**
 * DeprecatedOptions class.
 */
class DeprecatedOptions {
	/**
	 * Initialize.
	 */
	public static function init() {
		add_filter( 'pre_option_woocommerce_task_list_hidden', array( __CLASS__, 'get_deprecated_options' ), 10, 2 );
		add_filter( 'pre_option_woocommerce_extended_task_list_hidden', array( __CLASS__, 'get_deprecated_options' ), 10, 2 );
		add_action( 'pre_update_option_woocommerce_task_list_hidden', array( __CLASS__, 'update_deprecated_options' ), 10, 3 );
		add_action( 'pre_update_option_woocommerce_extended_task_list_hidden', array( __CLASS__, 'update_deprecated_options' ), 10, 3 );
	}

	/**
	 * Get the values from the correct source when attempting to retrieve deprecated options.
	 *
	 * @param string $pre_option Pre option value.
	 * @param string $option Option name.
	 * @return string
	 */
	public static function get_deprecated_options( $pre_option, $option ) {
		if ( defined( 'WC_INSTALLING' ) && WC_INSTALLING === true ) {
			return $pre_option;
		}

		$hidden = get_option( 'woocommerce_task_list_hidden_lists', array() );
		switch ( $option ) {
			case 'woocommerce_task_list_hidden':
				return in_array( 'setup', $hidden, true ) ? 'yes' : 'no';
			case 'woocommerce_extended_task_list_hidden':
				return in_array( 'extended', $hidden, true ) ? 'yes' : 'no';
		}
	}

	/**
	 * Updates the new option names when deprecated options are updated.
	 * This is a temporary fallback until we can fully remove the old task list components.
	 *
	 * @param string $value New value.
	 * @param string $old_value Old value.
	 * @param string $option Option name.
	 * @return string
	 */
	public static function update_deprecated_options( $value, $old_value, $option ) {
		switch ( $option ) {
			case 'woocommerce_task_list_hidden':
				$task_list = TaskLists::get_list( 'setup' );
				if ( ! $task_list ) {
					return;
				}
				$update = 'yes' === $value ? $task_list->hide() : $task_list->unhide();
				delete_option( 'woocommerce_task_list_hidden' );
				return false;
			case 'woocommerce_extended_task_list_hidden':
				$task_list = TaskLists::get_list( 'extended' );
				if ( ! $task_list ) {
					return;
				}
				$update = 'yes' === $value ? $task_list->hide() : $task_list->unhide();
				delete_option( 'woocommerce_extended_task_list_hidden' );
				return false;
		}
	}
}
Admin/Features/OnboardingTasks/TaskListSection.php000064400000004466152333011040016240 0ustar00<?php
/**
 * Handles storage and retrieval of a task list section
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

/**
 * Task List section class.
 *
 * @deprecated 7.2.0
 */
class TaskListSection {

	/**
	 * Title.
	 *
	 * @var string
	 */
	public $id = '';

	/**
	 * Title.
	 *
	 * @var string
	 */
	public $title = '';

	/**
	 * Description.
	 *
	 * @var string
	 */
	public $description = '';

	/**
	 * Image.
	 *
	 * @var string
	 */
	public $image = '';

	/**
	 * Tasks.
	 *
	 * @var array
	 */
	public $task_names = array();

	/**
	 * Parent task list.
	 *
	 * @var TaskList
	 */
	protected $task_list;

	/**
	 * Constructor
	 *
	 * @param array         $data Task list data.
	 * @param TaskList|null $task_list Parent task list.
	 */
	public function __construct( $data = array(), $task_list = null ) {
		$defaults = array(
			'id'          => '',
			'title'       => '',
			'description' => '',
			'image'       => '',
			'tasks'       => array(),
		);

		$data = wp_parse_args( $data, $defaults );

		$this->task_list   = $task_list;
		$this->id          = $data['id'];
		$this->title       = $data['title'];
		$this->description = $data['description'];
		$this->image       = $data['image'];
		$this->task_names  = $data['task_names'];
	}

	/**
	 * Returns if section is complete.
	 *
	 * @return boolean;
	 */
	private function is_complete() {
		$complete = true;
		foreach ( $this->task_names as $task_name ) {
			if ( null !== $this->task_list && isset( $this->task_list->task_class_id_map[ $task_name ] ) ) {
				$task = $this->task_list->get_task( $this->task_list->task_class_id_map[ $task_name ] );
				if ( $task->can_view() && ! $task->is_complete() ) {
					$complete = false;
					break;
				}
			}
		}
		return $complete;
	}

	/**
	 * Get the list for use in JSON.
	 *
	 * @return array
	 */
	public function get_json() {
		return array(
			'id'          => $this->id,
			'title'       => $this->title,
			'description' => $this->description,
			'image'       => $this->image,
			'tasks'       => array_map(
				function( $task_name ) {
					if ( null !== $this->task_list && isset( $this->task_list->task_class_id_map[ $task_name ] ) ) {
						return $this->task_list->task_class_id_map[ $task_name ];
					}
					return '';
				},
				$this->task_names
			),
			'isComplete'  => $this->is_complete(),
		);
	}
}
Admin/Features/OnboardingTasks/Init.php000064400000002116152333011040014046 0ustar00<?php
/**
 * WooCommerce Onboarding Tasks
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedOptions;

/**
 * Contains the logic for completing onboarding tasks.
 */
class Init {
	/**
	 * Class instance.
	 *
	 * @var OnboardingTasks instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor
	 */
	public function __construct() {
		DeprecatedOptions::init();
		TaskLists::init();
	}

	/**
	 * Get task item data for settings filter.
	 *
	 * @return array
	 */
	public static function get_settings() {
		$settings            = array();
		$wc_pay_is_connected = false;
		if ( class_exists( '\WC_Payments' ) ) {
			$wc_payments_gateway = \WC_Payments::get_gateway();
			$wc_pay_is_connected = method_exists( $wc_payments_gateway, 'is_connected' )
				? $wc_payments_gateway->is_connected()
				: false;
		}

		return $settings;
	}
}
Admin/Features/OnboardingTasks/TaskList.php000064400000022162152333011040014704 0ustar00<?php
/**
 * Handles storage and retrieval of a task list
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\WCAdminHelper;


/**
 * Task List class.
 */
class TaskList {
	/**
	 * Task traits.
	 */
	use TaskTraits;

	/**
	 * Option name hidden task lists.
	 */
	const HIDDEN_OPTION = 'woocommerce_task_list_hidden_lists';

	/**
	 * Option name of completed task lists.
	 */
	const COMPLETED_OPTION = 'woocommerce_task_list_completed_lists';

	/**
	 * Option name of hidden reminder bar.
	 */
	const REMINDER_BAR_HIDDEN_OPTION = 'woocommerce_task_list_reminder_bar_hidden';

	/**
	 * ID.
	 *
	 * @var string
	 */
	public $id = '';

	/**
	 * ID.
	 *
	 * @var string
	 */
	public $hidden_id = '';

	/**
	 * ID.
	 *
	 * @var boolean
	 */
	public $display_progress_header = false;

	/**
	 * Title.
	 *
	 * @var string
	 */
	public $title = '';

	/**
	 * Tasks.
	 *
	 * @var array
	 */
	public $tasks = array();

	/**
	 * Sort keys.
	 *
	 * @var array
	 */
	public $sort_by = array();

	/**
	 * Event prefix.
	 *
	 * @var string|null
	 */
	public $event_prefix = null;

	/**
	 * Task list visibility.
	 *
	 * @var boolean
	 */
	public $visible = true;

	/**
	 * Array of custom options.
	 *
	 * @var array
	 */
	public $options = array();

	/**
	 * Array of TaskListSection.
	 *
	 * @deprecated 7.2.0
	 *
	 * @var array
	 */
	private $sections = array();

	/**
	 * Key value map of task class and id used for sections.
	 *
	 * @deprecated 7.2.0
	 *
	 * @var array
	 */
	public $task_class_id_map = array();

	/**
	 * Constructor
	 *
	 * @param array $data Task list data.
	 */
	public function __construct( $data = array() ) {
		$defaults = array(
			'id'                      => null,
			'hidden_id'               => null,
			'title'                   => '',
			'tasks'                   => array(),
			'sort_by'                 => array(),
			'event_prefix'            => null,
			'options'                 => array(),
			'visible'                 => true,
			'display_progress_header' => false,
		);

		$data = wp_parse_args( $data, $defaults );

		$this->id                      = $data['id'];
		$this->hidden_id               = $data['hidden_id'];
		$this->title                   = $data['title'];
		$this->sort_by                 = $data['sort_by'];
		$this->event_prefix            = $data['event_prefix'];
		$this->options                 = $data['options'];
		$this->visible                 = $data['visible'];
		$this->display_progress_header = $data['display_progress_header'];

		foreach ( $data['tasks'] as $task_name ) {
			$class = 'Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\\' . $task_name;
			$task  = new $class( $this );
			$this->add_task( $task );
		}

		$this->possibly_remove_reminder_bar();
	}

	/**
	 * Check if the task list is hidden.
	 *
	 * @return bool
	 */
	public function is_hidden() {
		$hidden = get_option( self::HIDDEN_OPTION, array() );
		return in_array( $this->hidden_id ? $this->hidden_id : $this->id, $hidden, true );
	}

	/**
	 * Check if the task list is visible.
	 *
	 * @return bool
	 */
	public function is_visible() {
		if ( ! $this->visible || ! count( $this->get_viewable_tasks() ) > 0 ) {
			return false;
		}
		return ! $this->is_hidden();
	}

	/**
	 * Hide the task list.
	 *
	 * @return bool
	 */
	public function hide() {
		if ( $this->is_hidden() ) {
			return;
		}

		$viewable_tasks  = $this->get_viewable_tasks();
		$completed_count = array_reduce(
			$viewable_tasks,
			function( $total, $task ) {
				return $task->is_complete() ? $total + 1 : $total;
			},
			0
		);

		$this->record_tracks_event(
			'completed',
			array(
				'action'                => 'remove_card',
				'completed_task_count'  => $completed_count,
				'incomplete_task_count' => count( $viewable_tasks ) - $completed_count,
			)
		);

		$hidden   = get_option( self::HIDDEN_OPTION, array() );
		$hidden[] = $this->hidden_id ? $this->hidden_id : $this->id;
		$this->maybe_set_default_layout( $hidden );
		return update_option( self::HIDDEN_OPTION, array_unique( $hidden ) );
	}

	/**
	 * Sets the default homepage layout to two_columns if "setup" tasklist is completed or hidden.
	 *
	 * @param array $completed_or_hidden_tasklist_ids Array of tasklist ids.
	 */
	public function maybe_set_default_layout( $completed_or_hidden_tasklist_ids ) {
		if ( in_array( 'setup', $completed_or_hidden_tasklist_ids, true ) ) {
			update_option( 'woocommerce_default_homepage_layout', 'two_columns' );
		}
	}

	/**
	 * Undo hiding of the task list.
	 *
	 * @return bool
	 */
	public function unhide() {
		$hidden = get_option( self::HIDDEN_OPTION, array() );
		$hidden = array_diff( $hidden, array( $this->hidden_id ? $this->hidden_id : $this->id ) );
		return update_option( self::HIDDEN_OPTION, $hidden );
	}

	/**
	 * Check if all viewable tasks are complete.
	 *
	 * @return bool
	 */
	public function is_complete() {
		foreach ( $this->get_viewable_tasks() as $viewable_task ) {
			if ( $viewable_task->is_complete() === false ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Check if a task list has previously been marked as complete.
	 *
	 * @return bool
	 */
	public function has_previously_completed() {
		$complete = get_option( self::COMPLETED_OPTION, array() );
		return in_array( $this->get_list_id(), $complete, true );
	}

	/**
	 * Add task to the task list.
	 *
	 * @param Task $task Task class.
	 */
	public function add_task( $task ) {
		if ( ! is_subclass_of( $task, 'Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task' ) ) {
			return new \WP_Error(
				'woocommerce_task_list_invalid_task',
				__( 'Task is not a subclass of `Task`', 'woocommerce' )
			);
		}
		if ( array_search( $task, $this->tasks, true ) ) {
			return;
		}

		$this->tasks[] = $task;
	}

	/**
	 * Get only visible tasks in list.
	 *
	 * @param string $task_id id of task.
	 * @return Task
	 */
	public function get_task( $task_id ) {
		return current(
			array_filter(
				$this->tasks,
				function( $task ) use ( $task_id ) {
					return $task->get_id() === $task_id;
				}
			)
		);
	}

	/**
	 * Get only visible tasks in list.
	 *
	 * @return array
	 */
	public function get_viewable_tasks() {
		return array_values(
			array_filter(
				$this->tasks,
				function( $task ) {
					return $task->can_view();
				}
			)
		);
	}

	/**
	 * Get task list sections.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return array
	 */
	public function get_sections() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		return $this->sections;
	}

	/**
	 * Track list completion of viewable tasks.
	 */
	public function possibly_track_completion() {
		if ( ! $this->is_complete() ) {
			return;
		}

		if ( $this->has_previously_completed() ) {
			return;
		}

		$completed_lists   = get_option( self::COMPLETED_OPTION, array() );
		$completed_lists[] = $this->get_list_id();
		update_option( self::COMPLETED_OPTION, $completed_lists );
		$this->maybe_set_default_layout( $completed_lists );
		$this->record_tracks_event( 'tasks_completed' );
	}

	/**
	 * Sorts the attached tasks array.
	 *
	 * @param array $sort_by list of columns with sort order.
	 * @return TaskList returns $this, for chaining.
	 */
	public function sort_tasks( $sort_by = array() ) {
		$sort_by = count( $sort_by ) > 0 ? $sort_by : $this->sort_by;
		if ( 0 !== count( $sort_by ) ) {
			usort(
				$this->tasks,
				function( $a, $b ) use ( $sort_by ) {
					return Task::sort( $a, $b, $sort_by );
				}
			);
		}
		return $this;
	}

	/**
	 * Prefix event for track event naming.
	 *
	 * @param string $event_name Event name.
	 * @return string
	 */
	public function prefix_event( $event_name ) {
		if ( null !== $this->event_prefix ) {
			return $this->event_prefix . $event_name;
		}
		return $this->get_list_id() . '_tasklist_' . $event_name;
	}

	/**
	 * Returns option to keep completed task list.
	 *
	 * @return string
	 */
	public function get_keep_completed_task_list() {
		return get_option( 'woocommerce_task_list_keep_completed', 'no' );
	}

	/**
	 * Remove reminder bar four weeks after store creation.
	 */
	public static function possibly_remove_reminder_bar() {
		$bar_hidden            = get_option( self::REMINDER_BAR_HIDDEN_OPTION, 'no' );
		$active_for_four_weeks = WCAdminHelper::is_wc_admin_active_for( WEEK_IN_SECONDS * 4 );

		if ( 'yes' === $bar_hidden || ! $active_for_four_weeks ) {
			return;
		}

		update_option( self::REMINDER_BAR_HIDDEN_OPTION, 'yes' );
	}

	/**
	 * Get the list for use in JSON.
	 *
	 * @return array
	 */
	public function get_json() {
		$this->possibly_track_completion();
		$tasks_json = array();
		foreach ( $this->tasks as $task ) {
			$json = $task->get_json();
			if ( $json['canView'] ) {
				$tasks_json[] = $json;
			}
		}

		return array(
			'id'                    => $this->get_list_id(),
			'title'                 => $this->title,
			'isHidden'              => $this->is_hidden(),
			'isVisible'             => $this->is_visible(),
			'isComplete'            => $this->is_complete(),
			'tasks'                 => $tasks_json,
			'eventPrefix'           => $this->prefix_event( '' ),
			'displayProgressHeader' => $this->display_progress_header,
			'keepCompletedTaskList' => $this->get_keep_completed_task_list(),
		);
	}
}
Admin/Features/OnboardingTasks/TaskLists.php000064400000025416152333011040015074 0ustar00<?php
/**
 * Handles storage and retrieval of task lists
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedExtendedTask;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\ReviewShippingOptions;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\TourInAppMarketplace;
/**
 * Task Lists class.
 */
class TaskLists {
	/**
	 * Class instance.
	 *
	 * @var TaskLists instance
	 */
	protected static $instance = null;

	/**
	 * An array of all registered lists.
	 *
	 * @var array
	 */
	protected static $lists = array();

	/**
	 * Boolean value to indicate if default tasks have been added.
	 *
	 * @var boolean
	 */
	protected static $default_tasks_loaded = false;

	/**
	 * Array of default tasks.
	 *
	 * @var array
	 */
	const DEFAULT_TASKS = array(
		'StoreDetails',
		'Purchase',
		'Products',
		'WooCommercePayments',
		'Payments',
		'Tax',
		'Shipping',
		'Marketing',
		'Appearance',
		'AdditionalPayments',
		'ReviewShippingOptions',
		'GetMobileApp',
		'TourInAppMarketplace',
	);

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Initialize the task lists.
	 */
	public static function init() {
		self::init_default_lists();
		add_action( 'admin_init', array( __CLASS__, 'set_active_task' ), 5 );
		add_action( 'init', array( __CLASS__, 'init_tasks' ) );
		add_action( 'admin_menu', array( __CLASS__, 'menu_task_count' ) );
		add_filter( 'woocommerce_admin_shared_settings', array( __CLASS__, 'task_list_preloaded_settings' ), 20 );
	}

	/**
	 * Check if an experiment is the treatment or control.
	 *
	 * @param string $name Name prefix of experiment.
	 * @return bool
	 */
	public static function is_experiment_treatment( $name ) {
		$anon_id        = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : '';
		$allow_tracking = 'yes' === get_option( 'woocommerce_allow_tracking' );
		$abtest         = new \WooCommerce\Admin\Experimental_Abtest(
			$anon_id,
			'woocommerce',
			$allow_tracking
		);

		$date = new \DateTime();
		$date->setTimeZone( new \DateTimeZone( 'UTC' ) );

		$experiment_name = sprintf(
			'%s_%s_%s',
			$name,
			$date->format( 'Y' ),
			$date->format( 'm' )
		);
		return $abtest->get_variation( $experiment_name ) === 'treatment';
	}

	/**
	 * Initialize default lists.
	 */
	public static function init_default_lists() {
		self::add_list(
			array(
				'id'                      => 'setup',
				'title'                   => __( 'Get ready to start selling', 'woocommerce' ),
				'tasks'                   => array(
					'StoreDetails',
					'Purchase',
					'Products',
					'WooCommercePayments',
					'Payments',
					'Tax',
					'Shipping',
					'Marketing',
					'Appearance',
				),
				'display_progress_header' => true,
				'event_prefix'            => 'tasklist_',
				'options'                 => array(
					'use_completed_title' => true,
				),
				'visible'                 => true,
			)
		);

		self::add_list(
			array(
				'id'      => 'extended',
				'title'   => __( 'Things to do next', 'woocommerce' ),
				'sort_by' => array(
					array(
						'key'   => 'is_complete',
						'order' => 'asc',
					),
					array(
						'key'   => 'level',
						'order' => 'asc',
					),
				),
				'tasks'   => array(
					'AdditionalPayments',
					'GetMobileApp',
				),
			)
		);
		self::add_list(
			array(
				'id'           => 'setup_two_column',
				'hidden_id'    => 'setup',
				'title'        => __( 'Get ready to start selling', 'woocommerce' ),
				'tasks'        => array(
					'Products',
					'WooCommercePayments',
					'Payments',
					'Tax',
					'Shipping',
					'Marketing',
					'Appearance',
				),
				'event_prefix' => 'tasklist_',
			)
		);
		self::add_list(
			array(
				'id'           => 'extended_two_column',
				'hidden_id'    => 'extended',
				'title'        => __( 'Things to do next', 'woocommerce' ),
				'sort_by'      => array(
					array(
						'key'   => 'is_complete',
						'order' => 'asc',
					),
					array(
						'key'   => 'level',
						'order' => 'asc',
					),
				),
				'tasks'        => array(
					'AdditionalPayments',
					'GetMobileApp',
				),
				'event_prefix' => 'extended_tasklist_',
			)
		);

		if ( Features::is_enabled( 'shipping-smart-defaults' ) ) {
			self::add_task(
				'extended',
				new ReviewShippingOptions(
					self::get_list( 'extended' )
				)
			);

			self::add_task(
				'extended_two_column',
				new ReviewShippingOptions(
					self::get_list( 'extended_two_column' )
				)
			);

			// Tasklist that will never be shown in homescreen,
			// used for having tasks that are accessed by other means.
			self::add_list(
				array(
					'id'           => 'secret_tasklist',
					'hidden_id'    => 'setup',
					'tasks'        => array(
						'ExperimentalShippingRecommendation',
					),
					'event_prefix' => 'secret_tasklist_',
					'visible'      => false,
				)
			);
		}

		if ( ! wp_is_mobile() ) { // Permit In-App Marketplace Tour on desktops only.
			$tour_task = new TourInAppMarketplace();
			self::add_task( 'extended', $tour_task );
			self::add_task( 'extended_two_column', $tour_task );
		}
	}

	/**
	 * Initialize tasks.
	 */
	public static function init_tasks() {
		foreach ( self::DEFAULT_TASKS as $task ) {
			$class = 'Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\\' . $task;
			if ( ! method_exists( $class, 'init' ) ) {
				continue;
			}
			$class::init();
		}
	}

	/**
	 * Temporarily store the active task to persist across page loads when neccessary.
	 * Most tasks do not need this.
	 */
	public static function set_active_task() {
		if ( ! isset( $_GET[ Task::ACTIVE_TASK_TRANSIENT ] ) || ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore csrf ok.
			return;
		}
		$referer = wp_get_referer();
		if ( ! $referer || 0 !== strpos( $referer, wc_admin_url() ) ) {
			return;
		}

		$task_id = sanitize_title_with_dashes( wp_unslash( $_GET[ Task::ACTIVE_TASK_TRANSIENT ] ) ); // phpcs:ignore csrf ok.

		$task = self::get_task( $task_id );

		if ( ! $task ) {
			return;
		}

		$task->set_active();
	}

	/**
	 * Add a task list.
	 *
	 * @param array $args Task list properties.
	 * @return \WP_Error|TaskList
	 */
	public static function add_list( $args ) {
		if ( isset( self::$lists[ $args['id'] ] ) ) {
			return new \WP_Error(
				'woocommerce_task_list_exists',
				__( 'Task list ID already exists', 'woocommerce' )
			);
		}

		self::$lists[ $args['id'] ] = new TaskList( $args );
		return self::$lists[ $args['id'] ];
	}

	/**
	 * Add task to a given task list.
	 *
	 * @param string $list_id List ID to add the task to.
	 * @param array  $args Task properties.
	 * @return \WP_Error|Task
	 */
	public static function add_task( $list_id, $args ) {
		if ( ! isset( self::$lists[ $list_id ] ) ) {
			return new \WP_Error(
				'woocommerce_task_list_invalid_list',
				__( 'Task list ID does not exist', 'woocommerce' )
			);
		}

		self::$lists[ $list_id ]->add_task( $args );
	}

	/**
	 * Add default extended task lists.
	 *
	 * @param array $extended_tasks list of extended tasks.
	 */
	public static function maybe_add_extended_tasks( $extended_tasks ) {
		$tasks = $extended_tasks ?? array();

		foreach ( self::$lists as $task_list ) {
			if ( 'extended' !== substr( $task_list->id, 0, 8 ) ) {
				continue;
			}
			foreach ( $tasks as $args ) {
				$task = new DeprecatedExtendedTask( $task_list, $args );
				$task_list->add_task( $task );
			}
		}

	}

	/**
	 * Get all task lists.
	 *
	 * @return array
	 */
	public static function get_lists() {
		return self::$lists;
	}

	/**
	 * Get all task lists.
	 *
	 * @param array $ids list of task list ids.
	 * @return array
	 */
	public static function get_lists_by_ids( $ids ) {
		return array_filter(
			self::$lists,
			function( $list ) use ( $ids ) {
				return in_array( $list->get_list_id(), $ids, true );
			}
		);
	}

	/**
	 * Get all task list ids.
	 *
	 * @return array
	 */
	public static function get_list_ids() {
		return array_keys( self::$lists );
	}

	/**
	 * Clear all task lists.
	 */
	public static function clear_lists() {
		self::$lists = array();
		return self::$lists;
	}

	/**
	 * Get visible task lists.
	 */
	public static function get_visible() {
		return array_filter(
			self::get_lists(),
			function ( $task_list ) {
				return $task_list->is_visible();
			}
		);
	}


	/**
	 * Retrieve a task list by ID.
	 *
	 * @param String $id Task list ID.
	 *
	 * @return TaskList|null
	 */
	public static function get_list( $id ) {
		if ( isset( self::$lists[ $id ] ) ) {
			return self::$lists[ $id ];
		}

		return null;
	}

	/**
	 * Retrieve single task.
	 *
	 * @param String $id Task ID.
	 * @param String $task_list_id Task list ID.
	 *
	 * @return Object
	 */
	public static function get_task( $id, $task_list_id = null ) {
		$task_list = $task_list_id ? self::get_list( $task_list_id ) : null;

		if ( $task_list_id && ! $task_list ) {
			return null;
		}

		$tasks_to_search = $task_list ? $task_list->tasks : array_reduce(
			self::get_lists(),
			function ( $all, $curr ) {
				return array_merge( $all, $curr->tasks );
			},
			array()
		);

		foreach ( $tasks_to_search as $task ) {
			if ( $id === $task->get_id() ) {
				return $task;
			}
		}

		return null;
	}

	/**
	 * Return number of setup tasks remaining
	 *
	 * @return number
	 */
	public static function setup_tasks_remaining() {
		$setup_list = self::get_list( 'setup' );

		if ( ! $setup_list || $setup_list->is_hidden() || $setup_list->is_complete() ) {
			return;
		}

		$remaining_tasks = array_values(
			array_filter(
				$setup_list->get_viewable_tasks(),
				function( $task ) {
					return ! $task->is_complete();
				}
			)
		);

		return count( $remaining_tasks );
	}

	/**
	 * Add badge to homescreen menu item for remaining tasks
	 */
	public static function menu_task_count() {
		global $submenu;

		$tasks_count = self::setup_tasks_remaining();

		if ( ! $tasks_count || ! isset( $submenu['woocommerce'] ) ) {
			return;
		}

		foreach ( $submenu['woocommerce'] as $key => $menu_item ) {
			if ( 0 === strpos( $menu_item[0], _x( 'Home', 'Admin menu name', 'woocommerce' ) ) ) {
				$submenu['woocommerce'][ $key ][0] .= ' <span class="awaiting-mod update-plugins remaining-tasks-badge count-' . esc_attr( $tasks_count ) . '">' . number_format_i18n( $tasks_count ) . '</span>'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
				break;
			}
		}

	}

	/**
	 * Add visible list ids to component settings.
	 *
	 * @param array $settings Component settings.
	 *
	 * @return array
	 */
	public static function task_list_preloaded_settings( $settings ) {
		$settings['visibleTaskListIds'] = array_keys( self::get_visible() );

		return $settings;
	}
}
Admin/Features/Onboarding.php000064400000006254152333011040012144 0ustar00<?php
/**
 * WooCommerce Onboarding
 */

namespace Automattic\WooCommerce\Admin\Features;

use Automattic\WooCommerce\Admin\DeprecatedClassFacade;

/**
 * Contains backend logic for the onboarding profile and checklist feature.
 *
 * @deprecated since 6.3.0, use WooCommerce\Internal\Admin\Onboarding.
 */
class Onboarding extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Admin\Features\Onboarding';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '6.3.0';

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
	}

	/**
	 * Get a list of allowed industries for the onboarding wizard.
	 *
	 * @deprecated 6.3.0
	 * @return array
	 */
	public static function get_allowed_industries() {
		wc_deprecated_function( 'get_allowed_industries', '6.3', '\Automattic\WooCommerce\Internal\Admin\OnboardingIndustries::get_allowed_industries()' );
		return \Automattic\WooCommerce\Internal\Admin\OnboardingIndustries::get_allowed_industries();
	}

	/**
	 * Get a list of allowed product types for the onboarding wizard.
	 *
	 * @deprecated 6.3.0
	 * @return array
	 */
	public static function get_allowed_product_types() {
		wc_deprecated_function( 'get_allowed_product_types', '6.3', '\Automattic\WooCommerce\Internal\Admin\OnboardingProducts::get_allowed_product_types()' );
		return \Automattic\WooCommerce\Internal\Admin\OnboardingProducts::get_allowed_product_types();
	}

	/**
	 * Get a list of themes for the onboarding wizard.
	 *
	 * @deprecated 6.3.0
	 * @return array
	 */
	public static function get_themes() {
		wc_deprecated_function( 'get_themes', '6.3', '\Automattic\WooCommerce\Internal\Admin\OnboardingThemes::get_themes()' );
		return \Automattic\WooCommerce\Internal\Admin\OnboardingThemes::get_themes();
	}

	/**
	 * Get theme data used in onboarding theme browser.
	 *
	 * @deprecated 6.3.0
	 * @param WP_Theme $theme Theme to gather data from.
	 * @return array
	 */
	public static function get_theme_data( $theme ) {
		wc_deprecated_function( 'get_theme_data', '6.3', '\Automattic\WooCommerce\Internal\Admin\OnboardingThemes::get_theme_data()' );
		return \Automattic\WooCommerce\Internal\Admin\OnboardingThemes::get_theme_data();
	}

	/**
	 * Gets an array of themes that can be installed & activated via the onboarding wizard.
	 *
	 * @deprecated 6.3.0
	 * @return array
	 */
	public static function get_allowed_themes() {
		wc_deprecated_function( 'get_allowed_themes', '6.3', '\Automattic\WooCommerce\Internal\Admin\OnboardingThemes::get_allowed_themes()' );
		return \Automattic\WooCommerce\Internal\Admin\OnboardingThemes::get_allowed_themes();
	}

	/**
	 * Get dynamic product data from API.
	 *
	 * @deprecated 6.3.0
	 * @param array $product_types Array of product types.
	 * @return array
	 */
	public static function get_product_data( $product_types ) {
		wc_deprecated_function( 'get_product_data', '6.3', '\Automattic\WooCommerce\Internal\Admin\OnboardingProducts::get_product_data()' );
		return \Automattic\WooCommerce\Internal\Admin\OnboardingProducts::get_product_data();
	}
}
Admin/FeaturePlugin.php000064400000001673152333011040011056 0ustar00<?php
/**
 * WooCommerce Admin: Feature plugin main class.
 */

namespace Automattic\WooCommerce\Admin;

defined( 'ABSPATH' ) || exit;

/**
 * Feature plugin main class.
 *
 * @deprecated since 6.4.0
 */
class FeaturePlugin extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\FeaturePlugin';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '6.4.0';

	/**
	 * Constructor
	 *
	 * @return void
	 */
	protected function __construct() {}

	/**
	 * Get class instance.
	 *
	 * @return object Instance.
	 */
	final public static function instance() {
		return new static();
	}

	/**
	 * Init the feature plugin, only if we can detect both Gutenberg and WooCommerce.
	 *
	 * @deprecated 6.4.0
	 */
	public function init() {}
}
Admin/Marketing/InstalledExtensions.php000064400000041057152333011040014224 0ustar00<?php
/**
 * InstalledExtensions class file.
 */

namespace Automattic\WooCommerce\Admin\Marketing;

use Automattic\WooCommerce\Admin\PluginsHelper;

/**
 * Installed Marketing Extensions class.
 */
class InstalledExtensions {

	/**
	 * Gets an array of plugin data for the "Installed marketing extensions" card.
	 *
	 * Valid extensions statuses are: installed, activated, configured
	 */
	public static function get_data() {
		$data = [];

		$automatewoo   = self::get_automatewoo_extension_data();
		$aw_referral   = self::get_aw_referral_extension_data();
		$aw_birthdays  = self::get_aw_birthdays_extension_data();
		$mailchimp     = self::get_mailchimp_extension_data();
		$facebook      = self::get_facebook_extension_data();
		$pinterest     = self::get_pinterest_extension_data();
		$google        = self::get_google_extension_data();
		$amazon_ebay   = self::get_amazon_ebay_extension_data();
		$mailpoet      = self::get_mailpoet_extension_data();
		$creative_mail = self::get_creative_mail_extension_data();
		$tiktok        = self::get_tiktok_extension_data();
		$jetpack_crm   = self::get_jetpack_crm_extension_data();
		$zapier        = self::get_zapier_extension_data();
		$salesforce    = self::get_salesforce_extension_data();
		$vimeo         = self::get_vimeo_extension_data();
		$trustpilot    = self::get_trustpilot_extension_data();

		if ( $automatewoo ) {
			$data[] = $automatewoo;
		}

		if ( $aw_referral ) {
			$data[] = $aw_referral;
		}

		if ( $aw_birthdays ) {
			$data[] = $aw_birthdays;
		}

		if ( $mailchimp ) {
			$data[] = $mailchimp;
		}

		if ( $facebook ) {
			$data[] = $facebook;
		}

		if ( $pinterest ) {
			$data[] = $pinterest;
		}

		if ( $google ) {
			$data[] = $google;
		}

		if ( $amazon_ebay ) {
			$data[] = $amazon_ebay;
		}

		if ( $mailpoet ) {
			$data[] = $mailpoet;
		}

		if ( $creative_mail ) {
			$data[] = $creative_mail;
		}

		if ( $tiktok ) {
			$data[] = $tiktok;
		}

		if ( $jetpack_crm ) {
			$data[] = $jetpack_crm;
		}

		if ( $zapier ) {
			$data[] = $zapier;
		}

		if ( $salesforce ) {
			$data[] = $salesforce;
		}

		if ( $vimeo ) {
			$data[] = $vimeo;
		}

		if ( $trustpilot ) {
			$data[] = $trustpilot;
		}

		return $data;
	}

	/**
	 * Get allowed plugins.
	 *
	 * @return array
	 */
	public static function get_allowed_plugins() {
		return [
			'automatewoo',
			'mailchimp-for-woocommerce',
			'creative-mail-by-constant-contact',
			'facebook-for-woocommerce',
			'pinterest-for-woocommerce',
			'google-listings-and-ads',
			'hubspot-for-woocommerce',
			'woocommerce-amazon-ebay-integration',
			'mailpoet',
		];
	}

	/**
	 * Get AutomateWoo extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_automatewoo_extension_data() {
		$slug = 'automatewoo';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg';

		if ( 'activated' === $data['status'] && function_exists( 'AW' ) ) {
			$data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings' );
			$data['docsUrl']     = 'https://automatewoo.com/docs/';
			$data['status']      = 'configured'; // Currently no configuration step.
		}

		return $data;
	}

	/**
	 * Get AutomateWoo Refer a Friend extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_aw_referral_extension_data() {
		$slug = 'automatewoo-referrals';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg';

		if ( 'activated' === $data['status'] ) {
			$data['docsUrl'] = 'https://automatewoo.com/docs/refer-a-friend/';
			$data['status']  = 'configured';
			if ( function_exists( 'AW_Referrals' ) ) {
				$data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings&tab=referrals' );
			}
		}

		return $data;
	}

	/**
	 * Get AutomateWoo Birthdays extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_aw_birthdays_extension_data() {
		$slug = 'automatewoo-birthdays';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg';

		if ( 'activated' === $data['status'] ) {
			$data['docsUrl'] = 'https://automatewoo.com/docs/getting-started-with-birthdays/';
			$data['status']  = 'configured';
			if ( function_exists( 'AW_Birthdays' ) ) {
				$data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings&tab=birthdays' );
			}
		}

		return $data;
	}

	/**
	 * Get MailChimp extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_mailchimp_extension_data() {
		$slug = 'mailchimp-for-woocommerce';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/mailchimp.svg';

		if ( 'activated' === $data['status'] && function_exists( 'mailchimp_is_configured' ) ) {
			$data['docsUrl']     = 'https://mailchimp.com/help/connect-or-disconnect-mailchimp-for-woocommerce/';
			$data['settingsUrl'] = admin_url( 'admin.php?page=mailchimp-woocommerce' );

			if ( mailchimp_is_configured() ) {
				$data['status'] = 'configured';
			}
		}

		return $data;
	}

	/**
	 * Get Facebook extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_facebook_extension_data() {
		$slug = 'facebook-for-woocommerce';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/facebook-icon.svg';

		if ( $data['status'] === 'activated' && function_exists( 'facebook_for_woocommerce' ) ) {
			$integration = facebook_for_woocommerce()->get_integration();

			if ( $integration->is_configured() ) {
				$data['status'] = 'configured';
			}

			$data['settingsUrl'] = facebook_for_woocommerce()->get_settings_url();
			$data['docsUrl']     = facebook_for_woocommerce()->get_documentation_url();
		}

		return $data;
	}

	/**
	 * Get Pinterest extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_pinterest_extension_data() {
		$slug = 'pinterest-for-woocommerce';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/pinterest.svg';

		// TODO: Finalise docs url.
		$data['docsUrl'] = 'https://woocommerce.com/document/pinterest-for-woocommerce/?utm_medium=product';

		if ( 'activated' === $data['status'] && class_exists( 'Pinterest_For_Woocommerce' ) ) {
			$pinterest_onboarding_completed = Pinterest_For_Woocommerce()::is_setup_complete();
			if ( $pinterest_onboarding_completed ) {
				$data['status']      = 'configured';
				$data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/pinterest/settings' );
			} else {
				$data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/pinterest/landing' );
			}
		}

		return $data;
	}

	/**
	 * Get Google extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_google_extension_data() {
		$slug = 'google-listings-and-ads';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/google.svg';

		if ( 'activated' === $data['status'] && function_exists( 'woogle_get_container' ) && class_exists( '\Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService' ) ) {

			$merchant_center = woogle_get_container()->get( \Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService::class );

			if ( $merchant_center->is_setup_complete() ) {
				$data['status']      = 'configured';
				$data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/google/settings' );
			} else {
				$data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/google/start' );
			}

			$data['docsUrl'] = 'https://woocommerce.com/document/google-listings-and-ads/?utm_medium=product';
		}

		return $data;
	}

	/**
	 * Get Amazon / Ebay extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_amazon_ebay_extension_data() {
		$slug = 'woocommerce-amazon-ebay-integration';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/amazon-ebay.svg';

		if ( 'activated' === $data['status'] && class_exists( '\CodistoConnect' ) ) {

			$codisto_merchantid = get_option( 'codisto_merchantid' );

			// Use same check as codisto admin tabs.
			if ( is_numeric( $codisto_merchantid ) ) {
				$data['status'] = 'configured';
			}

			$data['settingsUrl'] = admin_url( 'admin.php?page=codisto-settings' );
			$data['docsUrl']     = 'https://woocommerce.com/document/multichannel-for-woocommerce-google-amazon-ebay-walmart-integration/?utm_medium=product';
		}

		return $data;
	}

	/**
	 * Get MailPoet extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_mailpoet_extension_data() {
		$slug = 'mailpoet';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/mailpoet.svg';

		if ( 'activated' === $data['status'] && class_exists( '\MailPoet\API\API' ) ) {
			$mailpoet_api = \MailPoet\API\API::MP( 'v1' );

			if ( ! method_exists( $mailpoet_api, 'isSetupComplete' ) || $mailpoet_api->isSetupComplete() ) {
				$data['status']      = 'configured';
				$data['settingsUrl'] = admin_url( 'admin.php?page=mailpoet-settings' );
			} else {
				$data['settingsUrl'] = admin_url( 'admin.php?page=mailpoet-newsletters' );
			}

			$data['docsUrl']    = 'https://kb.mailpoet.com/';
			$data['supportUrl'] = 'https://www.mailpoet.com/support/';
		}

		return $data;
	}

	/**
	 * Get Creative Mail for WooCommerce extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_creative_mail_extension_data() {
		$slug = 'creative-mail-by-constant-contact';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/creative-mail-by-constant-contact.png';

		if ( 'activated' === $data['status'] && class_exists( '\CreativeMail\Helpers\OptionsHelper' ) ) {
			if ( ! method_exists( '\CreativeMail\Helpers\OptionsHelper', 'get_instance_id' ) || \CreativeMail\Helpers\OptionsHelper::get_instance_id() !== null ) {
				$data['status']      = 'configured';
				$data['settingsUrl'] = admin_url( 'admin.php?page=creativemail_settings' );
			} else {
				$data['settingsUrl'] = admin_url( 'admin.php?page=creativemail' );
			}

			$data['docsUrl']    = 'https://app.creativemail.com/kb/help/WooCommerce';
			$data['supportUrl'] = 'https://app.creativemail.com/kb/help/';
		}

		return $data;
	}

	/**
	 * Get TikTok for WooCommerce extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_tiktok_extension_data() {
		$slug = 'tiktok-for-business';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/tiktok.jpg';

		if ( 'activated' === $data['status'] ) {
			if ( false !== get_option( 'tt4b_access_token' ) ) {
				$data['status'] = 'configured';
			}

			$data['settingsUrl'] = admin_url( 'admin.php?page=tiktok' );
			$data['docsUrl']     = 'https://woocommerce.com/document/tiktok-for-woocommerce/';
			$data['supportUrl']  = 'https://ads.tiktok.com/athena/user-feedback/?identify_key=6a1e079024806640c5e1e695d13db80949525168a052299b4970f9c99cb5ac78';
		}

		return $data;
	}

	/**
	 * Get Jetpack CRM for WooCommerce extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_jetpack_crm_extension_data() {
		$slug = 'zero-bs-crm';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/jetpack-crm.png';

		if ( 'activated' === $data['status'] ) {
			$data['status']      = 'configured';
			$data['settingsUrl'] = admin_url( 'admin.php?page=zerobscrm-plugin-settings' );
			$data['docsUrl']     = 'https://kb.jetpackcrm.com/';
			$data['supportUrl']  = 'https://kb.jetpackcrm.com/crm-support/';
		}

		return $data;
	}

	/**
	 * Get WooCommerce Zapier extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_zapier_extension_data() {
		$slug = 'woocommerce-zapier';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/zapier.png';

		if ( 'activated' === $data['status'] ) {
			$data['status']      = 'configured';
			$data['settingsUrl'] = admin_url( 'admin.php?page=wc-settings&tab=wc_zapier' );
			$data['docsUrl']     = 'https://docs.om4.io/woocommerce-zapier/';
		}

		return $data;
	}

	/**
	 * Get Salesforce extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_salesforce_extension_data() {
		$slug = 'integration-with-salesforce';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/salesforce.jpg';

		if ( 'activated' === $data['status'] && class_exists( '\Integration_With_Salesforce_Admin' ) ) {
			if ( ! method_exists( '\Integration_With_Salesforce_Admin', 'get_connection_status' ) || \Integration_With_Salesforce_Admin::get_connection_status() ) {
				$data['status'] = 'configured';
			}

			$data['settingsUrl'] = admin_url( 'admin.php?page=integration-with-salesforce' );
			$data['docsUrl']     = 'https://woocommerce.com/document/salesforce-integration/';
			$data['supportUrl']  = 'https://wpswings.com/submit-query/';
		}

		return $data;
	}

	/**
	 * Get Vimeo extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_vimeo_extension_data() {
		$slug = 'vimeo';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/vimeo.png';

		if ( 'activated' === $data['status'] && class_exists( '\Tribe\Vimeo_WP\Vimeo\Vimeo_Auth' ) ) {
			if ( method_exists( '\Tribe\Vimeo_WP\Vimeo\Vimeo_Auth', 'has_access_token' ) ) {
				$vimeo_auth = new \Tribe\Vimeo_WP\Vimeo\Vimeo_Auth();
				if ( $vimeo_auth->has_access_token() ) {
					$data['status'] = 'configured';
				}
			} else {
				$data['status'] = 'configured';
			}

			$data['settingsUrl'] = admin_url( 'options-general.php?page=vimeo_settings' );
			$data['docsUrl']     = 'https://woocommerce.com/document/vimeo/';
			$data['supportUrl']  = 'https://vimeo.com/help/contact';
		}

		return $data;
	}

	/**
	 * Get Trustpilot extension data.
	 *
	 * @return array|bool
	 */
	protected static function get_trustpilot_extension_data() {
		$slug = 'trustpilot-reviews';

		if ( ! PluginsHelper::is_plugin_installed( $slug ) ) {
			return false;
		}

		$data         = self::get_extension_base_data( $slug );
		$data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/trustpilot.png';

		if ( 'activated' === $data['status'] ) {
			$data['status']      = 'configured';
			$data['settingsUrl'] = admin_url( 'admin.php?page=woocommerce-trustpilot-settings-page' );
			$data['docsUrl']     = 'https://woocommerce.com/document/trustpilot-reviews/';
			$data['supportUrl']  = 'https://support.trustpilot.com/hc/en-us/requests/new';
		}

		return $data;
	}


	/**
	 * Get an array of basic data for a given extension.
	 *
	 * @param string $slug Plugin slug.
	 *
	 * @return array|false
	 */
	protected static function get_extension_base_data( $slug ) {
		$status      = PluginsHelper::is_plugin_active( $slug ) ? 'activated' : 'installed';
		$plugin_data = PluginsHelper::get_plugin_data( $slug );

		if ( ! $plugin_data ) {
			return false;
		}

		return [
			'slug'        => $slug,
			'status'      => $status,
			'name'        => $plugin_data['Name'],
			'description' => html_entity_decode( wp_trim_words( $plugin_data['Description'], 20 ) ),
			'supportUrl'  => 'https://woocommerce.com/my-account/create-a-ticket/?utm_medium=product',
		];
	}

}
Admin/ReportsSync.php000064400000013710152333011040010572 0ustar00<?php
/**
 * Report table sync related functions and actions.
 */

namespace Automattic\WooCommerce\Admin;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Internal\Admin\Schedulers\CustomersScheduler;
use Automattic\WooCommerce\Internal\Admin\Schedulers\OrdersScheduler;
use Automattic\WooCommerce\Internal\Admin\Schedulers\ImportScheduler;

/**
 * ReportsSync Class.
 */
class ReportsSync {
	/**
	 * Hook in sync methods.
	 */
	public static function init() {
		// Initialize scheduler hooks.
		foreach ( self::get_schedulers() as $scheduler ) {
			$scheduler::init();
		}
		add_action( 'woocommerce_update_product', array( __CLASS__, 'clear_stock_count_cache' ) );
		add_action( 'woocommerce_new_product', array( __CLASS__, 'clear_stock_count_cache' ) );
		add_action( 'update_option_woocommerce_notify_low_stock_amount', array( __CLASS__, 'clear_stock_count_cache' ) );
		add_action( 'update_option_woocommerce_notify_no_stock_amount', array( __CLASS__, 'clear_stock_count_cache' ) );
	}

	/**
	 * Get classes for syncing data.
	 *
	 * @return array
	 * @throws \Exception Throws exception when invalid data is found.
	 */
	public static function get_schedulers() {
		$schedulers = apply_filters(
			'woocommerce_analytics_report_schedulers',
			array(
				new CustomersScheduler(),
				new OrdersScheduler(),
			)
		);

		foreach ( $schedulers as $scheduler ) {
			if ( ! is_subclass_of( $scheduler, 'Automattic\WooCommerce\Internal\Admin\Schedulers\ImportScheduler' ) ) {
				throw new \Exception( __( 'Report sync schedulers should be derived from the Automattic\WooCommerce\Internal\Admin\Schedulers\ImportScheduler class.', 'woocommerce' ) );
			}
		}

		return $schedulers;
	}

	/**
	 * Returns true if an import is in progress.
	 *
	 * @return bool
	 */
	public static function is_importing() {
		foreach ( self::get_schedulers() as $scheduler ) {
			if ( $scheduler::is_importing() ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Regenerate data for reports.
	 *
	 * @param int|bool $days Number of days to import.
	 * @param bool     $skip_existing Skip exisiting records.
	 * @return string
	 */
	public static function regenerate_report_data( $days, $skip_existing ) {
		if ( self::is_importing() ) {
			return new \WP_Error( 'wc_admin_import_in_progress', __( 'An import is already in progress. Please allow the previous import to complete before beginning a new one.', 'woocommerce' ) );
		}

		self::reset_import_stats( $days, $skip_existing );
		foreach ( self::get_schedulers() as $scheduler ) {
			$scheduler::schedule_action( 'import_batch_init', array( $days, $skip_existing ) );
		}

		/**
		 * Fires when report data regeneration begins.
		 *
		 * @param int|bool $days Number of days to import.
		 * @param bool     $skip_existing Skip exisiting records.
		 */
		do_action( 'woocommerce_analytics_regenerate_init', $days, $skip_existing );

		return __( 'Report table data is being rebuilt. Please allow some time for data to fully populate.', 'woocommerce' );
	}

	/**
	 * Update the import stat totals and counts.
	 *
	 * @param int|bool $days Number of days to import.
	 * @param bool     $skip_existing Skip exisiting records.
	 */
	public static function reset_import_stats( $days, $skip_existing ) {
		$import_stats = get_option( ImportScheduler::IMPORT_STATS_OPTION, array() );
		$totals       = self::get_import_totals( $days, $skip_existing );

		foreach ( self::get_schedulers() as $scheduler ) {
			$import_stats[ $scheduler::$name ]['imported'] = 0;
			$import_stats[ $scheduler::$name ]['total']    = $totals[ $scheduler::$name ];
		}

		// Update imported from date if older than previous.
		$previous_import_date = isset( $import_stats['imported_from'] ) ? $import_stats['imported_from'] : null;
		$current_import_date  = $days ? gmdate( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) ) : -1;

		if ( ! $previous_import_date || -1 === $current_import_date || new \DateTime( $previous_import_date ) > new \DateTime( $current_import_date ) ) {
			$import_stats['imported_from'] = $current_import_date;
		}

		update_option( ImportScheduler::IMPORT_STATS_OPTION, $import_stats );
	}

	/**
	 * Get stats for current import.
	 *
	 * @return array
	 */
	public static function get_import_stats() {
		$import_stats                 = get_option( ImportScheduler::IMPORT_STATS_OPTION, array() );
		$import_stats['is_importing'] = self::is_importing();

		return $import_stats;
	}

	/**
	 * Get the import totals for all syncs.
	 *
	 * @param int|bool $days Number of days to import.
	 * @param bool     $skip_existing Skip exisiting records.
	 * @return array
	 */
	public static function get_import_totals( $days, $skip_existing ) {
		$totals = array();

		foreach ( self::get_schedulers() as $scheduler ) {
			$items                       = $scheduler::get_items( 1, 1, $days, $skip_existing );
			$totals[ $scheduler::$name ] = $items->total;
		}

		return $totals;
	}

	/**
	 * Clears all queued actions.
	 */
	public static function clear_queued_actions() {
		foreach ( self::get_schedulers() as $scheduler ) {
			$scheduler::clear_queued_actions();
		}
	}

	/**
	 * Delete all data for reports.
	 *
	 * @return string
	 */
	public static function delete_report_data() {
		// Cancel all pending import jobs.
		self::clear_queued_actions();

		foreach ( self::get_schedulers() as $scheduler ) {
			$scheduler::schedule_action( 'delete_batch_init', array() );
		}

		// Delete import options.
		delete_option( ImportScheduler::IMPORT_STATS_OPTION );

		return __( 'Report table data is being deleted.', 'woocommerce' );
	}

	/**
	 * Clear the count cache when products are added or updated, or when
	 * the no/low stock options are changed.
	 *
	 * @param int $id Post/product ID.
	 */
	public static function clear_stock_count_cache( $id ) {
		delete_transient( 'wc_admin_stock_count_lowstock' );
		delete_transient( 'wc_admin_product_count' );
		$status_options = wc_get_product_stock_status_options();
		foreach ( $status_options as $status => $label ) {
			delete_transient( 'wc_admin_stock_count_' . $status );
		}
	}
}
Admin/Notes/NotesUnavailableException.php000064400000000541152333011040014500 0ustar00<?php
/**
 * WooCommerce Admin Notes Unavailable Exception Class
 *
 * Exception class thrown when an attempt to use notes is made but notes are unavailable.
 */

namespace Automattic\WooCommerce\Admin\Notes;

defined( 'ABSPATH' ) || exit;

/**
 * Notes\NotesUnavailableException class.
 */
class NotesUnavailableException extends \WC_Data_Exception {}
Admin/Notes/Notes.php000064400000033451152333011040010463 0ustar00<?php
/**
 * Handles storage and retrieval of admin notes
 */

namespace Automattic\WooCommerce\Admin\Notes;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
/**
 * Admin Notes class.
 */
class Notes {
	/**
	 * Hook used for recurring "unsnooze" action.
	 */
	const UNSNOOZE_HOOK = 'wc_admin_unsnooze_admin_notes';

	/**
	 * Hook appropriate actions.
	 */
	public static function init() {
		add_action( 'admin_init', array( __CLASS__, 'schedule_unsnooze_notes' ) );
		add_action( 'admin_init', array( __CLASS__, 'possibly_delete_survey_notes' ) );
		add_action( 'update_option_woocommerce_show_marketplace_suggestions', array( __CLASS__, 'possibly_delete_marketing_notes' ), 10, 2 );
	}

	/**
	 * Get notes from the database.
	 *
	 * @param string $context Getting notes for what context. Valid values: view, edit.
	 * @param array  $args Arguments to pass to the query( e.g. per_page and page).
	 * @return array Array of arrays.
	 */
	public static function get_notes( $context = 'edit', $args = array() ) {
		$data_store = self::load_data_store();
		$raw_notes  = $data_store->get_notes( $args );
		$notes      = array();
		foreach ( (array) $raw_notes as $raw_note ) {
			try {
				$note = new Note( $raw_note );
				/**
				 * Filter the note from db. This is used to modify the note before it is returned.
				 *
				 * @since 6.9.0
				 * @param Note $note The note object from the database.
				 */
				$note                               = apply_filters( 'woocommerce_get_note_from_db', $note );
				$note_id                            = $note->get_id();
				$notes[ $note_id ]                  = $note->get_data();
				$notes[ $note_id ]['name']          = $note->get_name( $context );
				$notes[ $note_id ]['type']          = $note->get_type( $context );
				$notes[ $note_id ]['locale']        = $note->get_locale( $context );
				$notes[ $note_id ]['title']         = $note->get_title( $context );
				$notes[ $note_id ]['content']       = $note->get_content( $context );
				$notes[ $note_id ]['content_data']  = $note->get_content_data( $context );
				$notes[ $note_id ]['status']        = $note->get_status( $context );
				$notes[ $note_id ]['source']        = $note->get_source( $context );
				$notes[ $note_id ]['date_created']  = $note->get_date_created( $context );
				$notes[ $note_id ]['date_reminder'] = $note->get_date_reminder( $context );
				$notes[ $note_id ]['actions']       = $note->get_actions( $context );
				$notes[ $note_id ]['layout']        = $note->get_layout( $context );
				$notes[ $note_id ]['image']         = $note->get_image( $context );
				$notes[ $note_id ]['is_deleted']    = $note->get_is_deleted( $context );
			} catch ( \Exception $e ) {
				wc_caught_exception( $e, __CLASS__ . '::' . __FUNCTION__, array( $note_id ) );
			}
		}
		return $notes;
	}

	/**
	 * Get admin note using it's ID
	 *
	 * @param int $note_id Note ID.
	 * @return Note|bool
	 */
	public static function get_note( $note_id ) {
		if ( false !== $note_id ) {
			try {
				return new Note( $note_id );
			} catch ( \Exception $e ) {
				wc_caught_exception( $e, __CLASS__ . '::' . __FUNCTION__, array( $note_id ) );
				return false;
			}
		}

		return false;
	}

	/**
	 * Get admin note using its name.
	 *
	 * This is a shortcut for the common pattern of looking up note ids by name and then passing the first id to get_note().
	 * It will behave unpredictably when more than one note with the given name exists.
	 *
	 * @param string $note_name Note name.
	 * @return Note|bool
	 **/
	public static function get_note_by_name( $note_name ) {
		$data_store = self::load_data_store();
		$note_ids   = $data_store->get_notes_with_name( $note_name );

		if ( empty( $note_ids ) ) {
			return false;
		}

		return self::get_note( $note_ids[0] );
	}

	/**
	 * Get the total number of notes
	 *
	 * @param string $type Comma separated list of note types.
	 * @param string $status Comma separated list of statuses.
	 * @return int
	 */
	public static function get_notes_count( $type = array(), $status = array() ) {
		$data_store = self::load_data_store();
		return $data_store->get_notes_count( $type, $status );
	}

	/**
	 * Deletes admin notes with a given name.
	 *
	 * @param string|array $names Name(s) to search for.
	 */
	public static function delete_notes_with_name( $names ) {
		if ( is_string( $names ) ) {
			$names = array( $names );
		} elseif ( ! is_array( $names ) ) {
			return;
		}

		$data_store = self::load_data_store();

		foreach ( $names as $name ) {
			$note_ids = $data_store->get_notes_with_name( $name );
			foreach ( (array) $note_ids as $note_id ) {
				$note = self::get_note( $note_id );
				if ( $note ) {
					$note->delete();
				}
			}
		}
	}

	/**
	 * Update a note.
	 *
	 * @param Note  $note              The note that will be updated.
	 * @param array $requested_updates a list of requested updates.
	 */
	public static function update_note( $note, $requested_updates ) {
		$note_changed = false;
		if ( isset( $requested_updates['status'] ) ) {
			$note->set_status( $requested_updates['status'] );
			$note_changed = true;
		}

		if ( isset( $requested_updates['date_reminder'] ) ) {
			$note->set_date_reminder( $requested_updates['date_reminder'] );
			$note_changed = true;
		}

		if ( isset( $requested_updates['is_deleted'] ) ) {
			$note->set_is_deleted( $requested_updates['is_deleted'] );
			$note_changed = true;
		}

		if ( isset( $requested_updates['is_read'] ) ) {
			$note->set_is_read( $requested_updates['is_read'] );
			$note_changed = true;
		}

		if ( $note_changed ) {
			$note->save();
		}
	}

	/**
	 * Soft delete of a note.
	 *
	 * @param Note $note The note that will be deleted.
	 */
	public static function delete_note( $note ) {
		$note->set_is_deleted( 1 );
		$note->save();
	}

	/**
	 * Soft delete of all the admin notes. Returns the deleted items.
	 *
	 * @param array $args Arguments to pass to the query (ex: status).
	 * @return array Array of notes.
	 */
	public static function delete_all_notes( $args = array() ) {
		$data_store = self::load_data_store();
		$defaults   = array(
			'order'      => 'desc',
			'orderby'    => 'date_created',
			'per_page'   => 25,
			'page'       => 1,
			'type'       => array(
				Note::E_WC_ADMIN_NOTE_INFORMATIONAL,
				Note::E_WC_ADMIN_NOTE_MARKETING,
				Note::E_WC_ADMIN_NOTE_WARNING,
				Note::E_WC_ADMIN_NOTE_SURVEY,
			),
			'is_deleted' => 0,
		);
		$args       = wp_parse_args( $args, $defaults );
		// Here we filter for the same params we are using to show the note list in client side.
		$raw_notes = $data_store->get_notes( $args );

		$notes = array();
		foreach ( (array) $raw_notes as $raw_note ) {
			$note = self::get_note( $raw_note->note_id );
			if ( $note ) {
				self::delete_note( $note );
				array_push( $notes, $note );
			}
		}
		return $notes;
	}

	/**
	 * Clear note snooze status if the reminder date has been reached.
	 */
	public static function unsnooze_notes() {
		$data_store = self::load_data_store();
		$raw_notes  = $data_store->get_notes(
			array(
				'status' => array( Note::E_WC_ADMIN_NOTE_SNOOZED ),
			)
		);
		$now        = new \DateTime();

		foreach ( $raw_notes as $raw_note ) {
			$note = self::get_note( $raw_note->note_id );
			if ( false === $note ) {
				continue;
			}

			$date_reminder = $note->get_date_reminder( 'edit' );

			if ( $date_reminder < $now ) {
				$note->set_status( Note::E_WC_ADMIN_NOTE_UNACTIONED );
				$note->set_date_reminder( null );
				$note->save();
			}
		}
	}

	/**
	 * Schedule unsnooze notes event.
	 */
	public static function schedule_unsnooze_notes() {
		if ( ! wp_next_scheduled( self::UNSNOOZE_HOOK ) ) {
			wp_schedule_event( time() + 5, 'hourly', self::UNSNOOZE_HOOK );
		}
	}

	/**
	 * Unschedule unsnooze notes event.
	 */
	public static function clear_queued_actions() {
		wp_clear_scheduled_hook( self::UNSNOOZE_HOOK );
	}

	/**
	 * Delete marketing notes if marketing has been opted out.
	 *
	 * @param string $old_value Old value.
	 * @param string $value New value.
	 */
	public static function possibly_delete_marketing_notes( $old_value, $value ) {
		if ( 'no' !== $value ) {
			return;
		}

		$data_store = self::load_data_store();
		$note_ids   = $data_store->get_note_ids_by_type( Note::E_WC_ADMIN_NOTE_MARKETING );

		foreach ( $note_ids as $note_id ) {
			$note = self::get_note( $note_id );
			if ( $note ) {
				$note->delete();
			}
		}
	}

	/**
	 * Delete actioned survey notes.
	 */
	public static function possibly_delete_survey_notes() {
		$data_store = self::load_data_store();
		$note_ids   = $data_store->get_note_ids_by_type( Note::E_WC_ADMIN_NOTE_SURVEY );

		foreach ( $note_ids as $note_id ) {
			$note = self::get_note( $note_id );
			if ( $note && ( $note->get_status() === Note::E_WC_ADMIN_NOTE_ACTIONED ) ) {
				$note->set_is_deleted( 1 );
				$note->save();
			}
		}
	}

	/**
	 * Get the status of a given note by name.
	 *
	 * @param string $note_name Name of the note.
	 * @return string|bool The note status.
	 */
	public static function get_note_status( $note_name ) {
		$note = self::get_note_by_name( $note_name );

		if ( ! $note ) {
			return false;
		}

		return $note->get_status();
	}

	/**
	 * Get action by id.
	 *
	 * @param Note $note The note that has of the action.
	 * @param int  $action_id Action ID.
	 * @return object|bool The found action.
	 */
	public static function get_action_by_id( $note, $action_id ) {
		$actions      = $note->get_actions( 'edit' );
		$found_action = false;

		foreach ( $actions as $action ) {
			if ( $action->id === $action_id ) {
				$found_action = $action;
			}
		}
		return $found_action;
	}

	/**
	 * Trigger note action.
	 *
	 * @param Note   $note The note that has the triggered action.
	 * @param object $triggered_action The triggered action.
	 * @return Note|bool
	 */
	public static function trigger_note_action( $note, $triggered_action ) {
		/**
		 * Fires when an admin note action is taken.
		 *
		 * @param string $name The triggered action name.
		 * @param Note   $note The corresponding Note.
		 */
		do_action( 'woocommerce_note_action', $triggered_action->name, $note );

		/**
		 * Fires when an admin note action is taken.
		 * For more specific targeting of note actions.
		 *
		 * @param Note $note The corresponding Note.
		 */
		do_action( 'woocommerce_note_action_' . $triggered_action->name, $note );

		// Update the note with the status for this action.
		if ( ! empty( $triggered_action->status ) ) {
			$note->set_status( $triggered_action->status );
		}

		$note->save();

		$event_params = array(
			'note_name'    => $note->get_name(),
			'note_type'    => $note->get_type(),
			'note_title'   => $note->get_title(),
			'note_content' => $note->get_content(),
			'action_name'  => $triggered_action->name,
			'action_label' => $triggered_action->label,
			'screen'       => self::get_screen_name(),
		);

		if ( in_array( $note->get_type(), array( 'error', 'update' ), true ) ) {
			wc_admin_record_tracks_event( 'store_alert_action', $event_params );
		} else {
			self::record_tracks_event_without_cookies( 'inbox_action_click', $event_params );
		}

		return $note;
	}

	/**
	 * Record tracks event for a specific user.
	 *
	 * @param int    $user_id The user id we want to record for the event.
	 * @param string $event_name Name of the event to record.
	 * @param array  $params The params to send to the event recording.
	 */
	public static function record_tracks_event_with_user( $user_id, $event_name, $params ) {
		// We save the current user id to set it back after the event recording.
		$current_user_id = get_current_user_id();

		wp_set_current_user( $user_id );
		self::record_tracks_event_without_cookies( $event_name, $params );
		wp_set_current_user( $current_user_id );

	}

	/**
	 * Record tracks event without using cookies.
	 *
	 * @param string $event_name Name of the event to record.
	 * @param array  $params The params to send to the event recording.
	 */
	private static function record_tracks_event_without_cookies( $event_name, $params ) {
		// We save the cookie to set it back after the event recording.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$anon_id = isset( $_COOKIE['tk_ai'] ) ? $_COOKIE['tk_ai'] : null;

		unset( $_COOKIE['tk_ai'] );
		wc_admin_record_tracks_event( $event_name, $params );
		if ( isset( $anon_id ) ) {
			setcookie( 'tk_ai', $anon_id );
		}
	}

	/**
	 * Get screen name.
	 *
	 * @return string The screen name.
	 */
	public static function get_screen_name() {
		$screen_name = '';

		if ( isset( $_SERVER['HTTP_REFERER'] ) ) {
			parse_str( wp_parse_url( $_SERVER['HTTP_REFERER'], PHP_URL_QUERY ), $queries ); // phpcs:ignore sanitization ok.
		}
		if ( isset( $queries ) ) {
			$page      = isset( $queries['page'] ) ? $queries['page'] : null;
			$path      = isset( $queries['path'] ) ? $queries['path'] : null;
			$post_type = isset( $queries['post_type'] ) ? $queries['post_type'] : null;
			$post      = isset( $queries['post'] ) ? get_post_type( $queries['post'] ) : null;
		}

		if ( isset( $page ) ) {
			$current_page = 'wc-admin' === $page ? 'home_screen' : $page;
			$screen_name  = isset( $path ) ? substr( str_replace( '/', '_', $path ), 1 ) : $current_page;
		} elseif ( isset( $post_type ) ) {
			$screen_name = $post_type;
		} elseif ( isset( $post ) ) {
			$screen_name = $post;
		}
		return $screen_name;
	}

	/**
	 * Loads the data store.
	 *
	 * If the "admin-note" data store is unavailable, attempts to load it
	 * will result in an exception.
	 * This method catches that exception and throws a custom one instead.
	 *
	 * @return \WC_Data_Store The "admin-note" data store.
	 * @throws NotesUnavailableException Throws exception if data store loading fails.
	 */
	public static function load_data_store() {
		try {
			return \WC_Data_Store::load( 'admin-note' );
		} catch ( \Exception $e ) {
			throw new NotesUnavailableException(
				'woocommerce_admin_notes_unavailable',
				__( 'Notes are unavailable because the "admin-note" data store cannot be loaded.', 'woocommerce' )
			);
		}
	}
}
Admin/Notes/NoteTraits.php000064400000016143152333011040011466 0ustar00<?php
/**
 * WC Admin Note Traits
 *
 * WC Admin Note Traits class that houses shared functionality across notes.
 */

namespace Automattic\WooCommerce\Admin\Notes;

use Automattic\WooCommerce\Admin\WCAdminHelper;

defined( 'ABSPATH' ) || exit;

/**
 * NoteTraits class.
 */
trait NoteTraits {
	/**
	 * Test how long WooCommerce Admin has been active.
	 *
	 * @param int $seconds Time in seconds to check.
	 * @return bool Whether or not WooCommerce admin has been active for $seconds.
	 */
	private static function wc_admin_active_for( $seconds ) {
		return WCAdminHelper::is_wc_admin_active_for( $seconds );
	}

	/**
	 * Test if WooCommerce Admin has been active within a pre-defined range.
	 *
	 * @param string $range range available in WC_ADMIN_STORE_AGE_RANGES.
	 * @param int    $custom_start custom start in range.
	 * @return bool Whether or not WooCommerce admin has been active within the range.
	 */
	private static function is_wc_admin_active_in_date_range( $range, $custom_start = null ) {
		return WCAdminHelper::is_wc_admin_active_in_date_range( $range, $custom_start );
	}

	/**
	 * Check if the note has been previously added.
	 *
	 * @throws NotesUnavailableException Throws exception when notes are unavailable.
	 */
	public static function note_exists() {
		$data_store = Notes::load_data_store();
		$note_ids   = $data_store->get_notes_with_name( self::NOTE_NAME );
		return ! empty( $note_ids );
	}

	/**
	 * Checks if a note can and should be added.
	 *
	 * @return bool
	 * @throws NotesUnavailableException Throws exception when notes are unavailable.
	 */
	public static function can_be_added() {
		$note = self::get_note();

		if ( ! $note instanceof Note && ! $note instanceof WC_Admin_Note ) {
			return;
		}

		if ( self::note_exists() ) {
			return false;
		}

		if (
			'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) &&
			Note::E_WC_ADMIN_NOTE_MARKETING === $note->get_type()
		) {
			return false;
		}

		return true;
	}

	/**
	 * Add the note if it passes predefined conditions.
	 *
	 * @throws NotesUnavailableException Throws exception when notes are unavailable.
	 */
	public static function possibly_add_note() {
		$note = self::get_note();

		if ( ! self::can_be_added() ) {
			return;
		}

		$note->save();
	}

	/**
	 * Alias this method for backwards compatibility.
	 *
	 * @throws NotesUnavailableException Throws exception when notes are unavailable.
	 */
	public static function add_note() {
		self::possibly_add_note();
	}

	/**
	 * Should this note exist? (Default implementation is generous. Override as needed.)
	 */
	public static function is_applicable() {
		return true;
	}

	/**
	 * Delete this note if it is not applicable, unless has been soft-deleted or actioned already.
	 */
	public static function delete_if_not_applicable() {
		if ( ! self::is_applicable() ) {
			$data_store = Notes::load_data_store();
			$note_ids   = $data_store->get_notes_with_name( self::NOTE_NAME );

			if ( ! empty( $note_ids ) ) {
				$note = Notes::get_note( $note_ids[0] );

				if ( ! $note->get_is_deleted() && ( Note::E_WC_ADMIN_NOTE_ACTIONED !== $note->get_status() ) ) {
					return self::possibly_delete_note();
				}
			}
		}
	}

	/**
	 * Possibly delete the note, if it exists in the database. Note that this
	 * is a hard delete, for where it doesn't make sense to soft delete or
	 * action the note.
	 *
	 * @throws NotesUnavailableException Throws exception when notes are unavailable.
	 */
	public static function possibly_delete_note() {
		$data_store = Notes::load_data_store();
		$note_ids   = $data_store->get_notes_with_name( self::NOTE_NAME );

		foreach ( $note_ids as $note_id ) {
			$note = Notes::get_note( $note_id );

			if ( $note ) {
				$data_store->delete( $note );
			}
		}
	}


	/**
	 * Update the note if it passes predefined conditions.
	 *
	 * @throws NotesUnavailableException Throws exception when notes are unavailable.
	 */
	public static function possibly_update_note() {
		$note_in_db = Notes::get_note_by_name( self::NOTE_NAME );
		if ( ! $note_in_db ) {
			return;
		}

		if ( ! method_exists( self::class, 'get_note' ) ) {
			return;
		}

		$note = self::get_note();
		if ( ! $note instanceof Note && ! $note instanceof WC_Admin_Note ) {
			return;
		}

		$need_save = in_array(
			true,
			array(
				self::update_note_field_if_changed( $note_in_db, $note, 'title' ),
				self::update_note_field_if_changed( $note_in_db, $note, 'content' ),
				self::update_note_field_if_changed( $note_in_db, $note, 'content_data' ),
				self::update_note_field_if_changed( $note_in_db, $note, 'type' ),
				self::update_note_field_if_changed( $note_in_db, $note, 'locale' ),
				self::update_note_field_if_changed( $note_in_db, $note, 'source' ),
				self::update_note_field_if_changed( $note_in_db, $note, 'actions' )
			),
			true
		);

		if ( $need_save ) {
			$note_in_db->save();
		}
	}


	/**
	 * Get if the note has been actioned.
	 *
	 * @return bool
	 * @throws NotesUnavailableException Throws exception when notes are unavailable.
	 */
	public static function has_note_been_actioned() {
		$data_store = Notes::load_data_store();
		$note_ids   = $data_store->get_notes_with_name( self::NOTE_NAME );

		if ( ! empty( $note_ids ) ) {
			$note = Notes::get_note( $note_ids[0] );

			if ( Note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) {
				return true;
			}
		}

		return false;
	}

/**
	 * Update a note field of note1 if it's different from note2 with getter and setter.
	 *
	 * @param Note   $note1 Note to update.
	 * @param Note   $note2 Note to compare against.
	 * @param string $field_name Field to update.
	 * @return bool True if the field was updated.
	 */
	private static function update_note_field_if_changed( $note1, $note2, $field_name ) {
		// We need to serialize the stdObject to compare it.
		$note1_field_value = self::possibly_convert_object_to_array(
			call_user_func( array( $note1, 'get_' . $field_name ) )
		);
		$note2_field_value = self::possibly_convert_object_to_array(
			call_user_func( array( $note2, 'get_' . $field_name ) )
		);

		if ( 'actions' === $field_name ) {
			// We need to individually compare the action fields because action object from db is different from action object of note.
			// For example, action object from db has "id".
			$diff        = array_udiff(
				$note1_field_value,
				$note2_field_value,
				function( $action1, $action2 ) {
					if ( $action1->name === $action2->name &&
						$action1->label === $action2->label &&
						$action1->query === $action2->query ) {
						return 0;
					}
					return -1;
				}
			);
			$need_update = count( $diff ) > 0;
		} else {
			$need_update = $note1_field_value !== $note2_field_value;
		}

		if ( $need_update ) {
			call_user_func(
				array( $note1, 'set_' . $field_name ),
				// Get note2 field again because it may have been changed during the comparison.
				call_user_func( array( $note2, 'get_' . $field_name ) )
			);
			return true;
		}
		return false;
	}

	/**
	 * Convert a value to array if it's a stdClass.
	 *
	 * @param mixed $obj variable to convert.
	 * @return mixed
	 */
	private static function possibly_convert_object_to_array( $obj ) {
		if ( $obj instanceof \stdClass ) {
			return (array) $obj;
		}
		return $obj;
	}
}
Admin/Notes/DataStore.php000064400000042547152333011040011267 0ustar00<?php
/**
 * WC Admin Note Data_Store class file.
 */

namespace Automattic\WooCommerce\Admin\Notes;

defined( 'ABSPATH' ) || exit;

/**
 * WC Admin Note Data Store (Custom Tables)
 */
class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
	// Extensions should define their own contexts and use them to avoid applying woocommerce_note_where_clauses when not needed.
	const WC_ADMIN_NOTE_OPER_GLOBAL = 'global';

	/**
	 * Method to create a new note in the database.
	 *
	 * @param Note $note Admin note.
	 */
	public function create( &$note ) {
		$date_created = time();
		$note->set_date_created( $date_created );

		global $wpdb;

		$note_to_be_inserted = array(
			'name'         => $note->get_name(),
			'type'         => $note->get_type(),
			'locale'       => $note->get_locale(),
			'title'        => $note->get_title(),
			'content'      => $note->get_content(),
			'status'       => $note->get_status(),
			'source'       => $note->get_source(),
			'is_snoozable' => (int) $note->get_is_snoozable(),
			'layout'       => $note->get_layout(),
			'image'        => $note->get_image(),
			'is_deleted'   => (int) $note->get_is_deleted(),
		);

		$note_to_be_inserted['content_data']  = wp_json_encode( $note->get_content_data() );
		$note_to_be_inserted['date_created']  = gmdate( 'Y-m-d H:i:s', $date_created );
		$note_to_be_inserted['date_reminder'] = null;

		$wpdb->insert( $wpdb->prefix . 'wc_admin_notes', $note_to_be_inserted );
		$note_id = $wpdb->insert_id;
		$note->set_id( $note_id );
		$this->save_actions( $note );
		$note->apply_changes();

		/**
		 * Fires when an admin note is created.
		 *
		 * @param int $note_id Note ID.
		 */
		do_action( 'woocommerce_note_created', $note_id );
	}

	/**
	 * Method to read a note.
	 *
	 * @param Note $note Admin note.
	 * @throws \Exception Throws exception when invalid data is found.
	 */
	public function read( &$note ) {
		global $wpdb;

		$note->set_defaults();
		$note_row = false;

		$note_id = $note->get_id();
		if ( 0 !== $note_id || '0' !== $note_id ) {
			$note_row = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT * FROM {$wpdb->prefix}wc_admin_notes WHERE note_id = %d LIMIT 1",
					$note->get_id()
				)
			);
		}

		if ( 0 === $note->get_id() || '0' === $note->get_id() ) {
			$this->read_actions( $note );
			$note->set_object_read( true );

			/**
			 * Fires when an admin note is loaded.
			 *
			 * @param int $note_id Note ID.
			 */
			do_action( 'woocommerce_note_loaded', $note );
		} elseif ( $note_row ) {
			$note->set_name( $note_row->name );
			$note->set_type( $note_row->type );
			$note->set_locale( $note_row->locale );
			$note->set_title( $note_row->title );
			$note->set_content( $note_row->content );

			// The default for 'content_value' used to be an array, so there might be rows with invalid data!
			$content_data = json_decode( $note_row->content_data );
			if ( ! $content_data ) {
				$content_data = new \stdClass();
			} elseif ( is_array( $content_data ) ) {
				$content_data = (object) $content_data;
			}
			$note->set_content_data( $content_data );

			$note->set_status( $note_row->status );
			$note->set_source( $note_row->source );
			$note->set_date_created( $note_row->date_created );
			$note->set_date_reminder( $note_row->date_reminder );
			$note->set_is_snoozable( $note_row->is_snoozable );
			$note->set_is_deleted( (bool) $note_row->is_deleted );
			isset( $note_row->is_read ) && $note->set_is_read( (bool) $note_row->is_read );
			$note->set_layout( $note_row->layout );
			$note->set_image( $note_row->image );
			$this->read_actions( $note );
			$note->set_object_read( true );

			/**
			 * Fires when an admin note is loaded.
			 *
			 * @param int $note_id Note ID.
			 */
			do_action( 'woocommerce_note_loaded', $note );
		} else {
			throw new \Exception( __( 'Invalid admin note', 'woocommerce' ) );
		}
	}

	/**
	 * Updates a note in the database.
	 *
	 * @param Note $note Admin note.
	 */
	public function update( &$note ) {
		global $wpdb;

		if ( $note->get_id() ) {
			$date_created           = $note->get_date_created();
			$date_created_timestamp = $date_created->getTimestamp();
			$date_created_to_db     = gmdate( 'Y-m-d H:i:s', $date_created_timestamp );

			$date_reminder = $note->get_date_reminder();
			if ( is_null( $date_reminder ) ) {
				$date_reminder_to_db = null;
			} else {
				$date_reminder_timestamp = $date_reminder->getTimestamp();
				$date_reminder_to_db     = gmdate( 'Y-m-d H:i:s', $date_reminder_timestamp );
			}

			$wpdb->update(
				$wpdb->prefix . 'wc_admin_notes',
				array(
					'name'          => $note->get_name(),
					'type'          => $note->get_type(),
					'locale'        => $note->get_locale(),
					'title'         => $note->get_title(),
					'content'       => $note->get_content(),
					'content_data'  => wp_json_encode( $note->get_content_data() ),
					'status'        => $note->get_status(),
					'source'        => $note->get_source(),
					'date_created'  => $date_created_to_db,
					'date_reminder' => $date_reminder_to_db,
					'is_snoozable'  => $note->get_is_snoozable(),
					'layout'        => $note->get_layout(),
					'image'         => $note->get_image(),
					'is_deleted'    => $note->get_is_deleted(),
					'is_read'       => $note->get_is_read(),
				),
				array( 'note_id' => $note->get_id() )
			);
		}

		$this->save_actions( $note );
		$note->apply_changes();

		/**
		 * Fires when an admin note is updated.
		 *
		 * @param int $note_id Note ID.
		 */
		do_action( 'woocommerce_note_updated', $note->get_id() );
	}

	/**
	 * Deletes a note from the database.
	 *
	 * @param Note  $note Admin note.
	 * @param array $args Array of args to pass to the delete method (not used).
	 */
	public function delete( &$note, $args = array() ) {
		$note_id = $note->get_id();
		if ( $note_id ) {
			global $wpdb;
			$wpdb->delete( $wpdb->prefix . 'wc_admin_notes', array( 'note_id' => $note_id ) );
			$wpdb->delete( $wpdb->prefix . 'wc_admin_note_actions', array( 'note_id' => $note_id ) );
			$note->set_id( null );
		}

		/**
		 * Fires when an admin note is deleted.
		 *
		 * @param int $note_id Note ID.
		 */
		do_action( 'woocommerce_note_deleted', $note_id );
	}

	/**
	 * Read actions from the database.
	 *
	 * @param Note $note Admin note.
	 */
	private function read_actions( &$note ) {
		global $wpdb;

		$db_actions = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT action_id, name, label, query, status, actioned_text, nonce_action, nonce_name
				FROM {$wpdb->prefix}wc_admin_note_actions
				WHERE note_id = %d",
				$note->get_id()
			)
		);

		$note_actions = array();

		if ( $db_actions ) {
			foreach ( $db_actions as $action ) {
				$note_actions[] = (object) array(
					'id'            => (int) $action->action_id,
					'name'          => $action->name,
					'label'         => $action->label,
					'query'         => $action->query,
					'status'        => $action->status,
					'actioned_text' => $action->actioned_text,
					'nonce_action'  => $action->nonce_action,
					'nonce_name'    => $action->nonce_name,
				);
			}
		}

		$note->set_actions( $note_actions );
	}

	/**
	 * Save actions to the database.
	 * This function clears old actions, then re-inserts new if any changes are found.
	 *
	 * @param Note $note Note object.
	 *
	 * @return bool|void
	 */
	private function save_actions( &$note ) {
		global $wpdb;

		$changed_props = array_keys( $note->get_changes() );

		if ( ! in_array( 'actions', $changed_props, true ) ) {
			return false;
		}

		// Process action removal. Actions are removed from
		// the note if they aren't part of the changeset.
		// See Note::add_action().
		$changed_actions = $note->get_actions( 'edit' );
		$actions_to_keep = array();

		foreach ( $changed_actions as $action ) {
			if ( ! empty( $action->id ) ) {
				$actions_to_keep[] = (int) $action->id;
			}
		}

		$clear_actions_query = $wpdb->prepare(
			"DELETE FROM {$wpdb->prefix}wc_admin_note_actions WHERE note_id = %d",
			$note->get_id()
		);

		if ( $actions_to_keep ) {
			$clear_actions_query .= sprintf( ' AND action_id NOT IN (%s)', implode( ',', $actions_to_keep ) );
		}

		$wpdb->query( $clear_actions_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		// Update/insert the actions in this changeset.
		foreach ( $changed_actions as $action ) {
			$action_data = array(
				'note_id'       => $note->get_id(),
				'name'          => $action->name,
				'label'         => $action->label,
				'query'         => $action->query,
				'status'        => $action->status,
				'actioned_text' => $action->actioned_text,
				'nonce_action'  => $action->nonce_action,
				'nonce_name'    => $action->nonce_name,
			);

			$data_format = array(
				'%d',
				'%s',
				'%s',
				'%s',
				'%s',
				'%s',
				'%s',
				'%s',
			);

			if ( ! empty( $action->id ) ) {
				$action_data['action_id'] = $action->id;
				$data_format[]            = '%d';
			}

			$wpdb->replace(
				$wpdb->prefix . 'wc_admin_note_actions',
				$action_data,
				$data_format
			);
		}

		// Update actions from DB (to grab new IDs).
		$this->read_actions( $note );
	}

	/**
	 * Return an ordered list of notes.
	 *
	 * @param array  $args Query arguments.
	 * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed.
	 * @return array An array of objects containing a note id.
	 */
	public function get_notes( $args = array(), $context = self::WC_ADMIN_NOTE_OPER_GLOBAL ) {
		global $wpdb;

		$defaults = array(
			'per_page' => get_option( 'posts_per_page' ),
			'page'     => 1,
			'order'    => 'DESC',
			'orderby'  => 'date_created',
		);
		$args     = wp_parse_args( $args, $defaults );

		$offset        = $args['per_page'] * ( $args['page'] - 1 );
		$where_clauses = $this->get_notes_where_clauses( $args, $context );

		// sanitize order and orderby.
		$order_by  = '`' . str_replace( '`', '', $args['orderby'] ) . '`';
		$order_dir = 'asc' === strtolower( $args['order'] ) ? 'ASC' : 'DESC';

		$query = $wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT * FROM {$wpdb->prefix}wc_admin_notes WHERE 1=1{$where_clauses} ORDER BY {$order_by} {$order_dir} LIMIT %d, %d",
			$offset,
			$args['per_page']
		);

		return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Return an ordered list of notes, without paging or applying the 'woocommerce_note_where_clauses' filter.
	 * INTERNAL: This method is not intended to be used by external code, and may change without notice.
	 *
	 * @param array $args Query arguments.
	 * @return array An array of database records.
	 */
	public function lookup_notes( $args = array() ) {
		global $wpdb;

		$defaults = array(
			'order'   => 'DESC',
			'orderby' => 'date_created',
		);
		$args     = wp_parse_args( $args, $defaults );

		$where_clauses = $this->args_to_where_clauses( $args );

		// sanitize order and orderby.
		$order_by  = '`' . str_replace( '`', '', $args['orderby'] ) . '`';
		$order_dir = 'asc' === strtolower( $args['order'] ) ? 'ASC' : 'DESC';

		$query = "SELECT * FROM {$wpdb->prefix}wc_admin_notes WHERE 1=1{$where_clauses} ORDER BY {$order_by} {$order_dir}";

		return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Return a count of notes.
	 *
	 * @param string $type Comma separated list of note types.
	 * @param string $status Comma separated list of statuses.
	 * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed.
	 * @return array An array of objects containing a note id.
	 */
	public function get_notes_count( $type = array(), $status = array(), $context = self::WC_ADMIN_NOTE_OPER_GLOBAL ) {
		global $wpdb;

		$where_clauses = $this->get_notes_where_clauses(
			array(
				'type'   => $type,
				'status' => $status,
			),
			$context
		);

		if ( ! empty( $where_clauses ) ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_admin_notes WHERE 1=1{$where_clauses}" );
		}

		return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_admin_notes" );
	}

	/**
	 * Parses the query arguments passed in as arrays and escapes the values.
	 *
	 * @param array      $args the query arguments.
	 * @param string     $key the key of the specific argument.
	 * @param array|null $allowed_types optional allowed_types if only a specific set is allowed.
	 * @return array the escaped array of argument values.
	 */
	private function get_escaped_arguments_array_by_key( $args = array(), $key = '', $allowed_types = null ) {
		$arg_array = array();
		if ( isset( $args[ $key ] ) ) {
			foreach ( $args[ $key ] as $args_type ) {
				$args_type = trim( $args_type );
				$allowed   = is_null( $allowed_types ) || in_array( $args_type, $allowed_types, true );
				if ( $allowed ) {
					$arg_array[] = sprintf( "'%s'", esc_sql( $args_type ) );
				}
			}
		}
		return $arg_array;
	}

	/**
	 * Return where clauses for getting notes by status and type. For use in both the count and listing queries.
	 * Applies woocommerce_note_where_clauses filter.
	 *
	 * @uses args_to_where_clauses
	 * @param array  $args Array of args to pass.
	 * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed.
	 * @return string Where clauses for the query.
	 */
	public function get_notes_where_clauses( $args = array(), $context = self::WC_ADMIN_NOTE_OPER_GLOBAL ) {
		$where_clauses = $this->args_to_where_clauses( $args );

		/**
		 * Filter the notes WHERE clause before retrieving the data.
		 *
		 * Allows modification of the notes select criterial.
		 *
		 * @param string $where_clauses The generated WHERE clause.
		 * @param array  $args          The original arguments for the request.
		 * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed.
		 */
		return apply_filters( 'woocommerce_note_where_clauses', $where_clauses, $args, $context );
	}

	/**
	 * Return where clauses for notes queries without applying woocommerce_note_where_clauses filter.
	 * INTERNAL: This method is not intended to be used by external code, and may change without notice.
	 *
	 * @param array $args Array of arguments for query conditionals.
	 * @return string Where clauses.
	 */
	protected function args_to_where_clauses( $args = array() ) {
		$allowed_types    = Note::get_allowed_types();
		$where_type_array = $this->get_escaped_arguments_array_by_key( $args, 'type', $allowed_types );

		$allowed_statuses   = Note::get_allowed_statuses();
		$where_status_array = $this->get_escaped_arguments_array_by_key( $args, 'status', $allowed_statuses );

		$escaped_is_deleted = '';
		if ( isset( $args['is_deleted'] ) ) {
			$escaped_is_deleted = esc_sql( $args['is_deleted'] );
		}

		$where_name_array          = $this->get_escaped_arguments_array_by_key( $args, 'name' );
		$where_excluded_name_array = $this->get_escaped_arguments_array_by_key( $args, 'excluded_name' );
		$where_source_array        = $this->get_escaped_arguments_array_by_key( $args, 'source' );

		$escaped_where_types          = implode( ',', $where_type_array );
		$escaped_where_status         = implode( ',', $where_status_array );
		$escaped_where_names          = implode( ',', $where_name_array );
		$escaped_where_excluded_names = implode( ',', $where_excluded_name_array );
		$escaped_where_source         = implode( ',', $where_source_array );
		$where_clauses                = '';

		if ( ! empty( $escaped_where_types ) ) {
			$where_clauses .= " AND type IN ($escaped_where_types)";
		}

		if ( ! empty( $escaped_where_status ) ) {
			$where_clauses .= " AND status IN ($escaped_where_status)";
		}

		if ( ! empty( $escaped_where_names ) ) {
			$where_clauses .= " AND name IN ($escaped_where_names)";
		}

		if ( ! empty( $escaped_where_excluded_names ) ) {
			$where_clauses .= " AND name NOT IN ($escaped_where_excluded_names)";
		}

		if ( ! empty( $escaped_where_source ) ) {
			$where_clauses .= " AND source IN ($escaped_where_source)";
		}

		$where_clauses .= $escaped_is_deleted ? ' AND is_deleted = 1' : ' AND is_deleted = 0';

		return $where_clauses;
	}

	/**
	 * Find all the notes with a given name.
	 *
	 * @param string $name Name to search for.
	 * @return array An array of matching note ids.
	 */
	public function get_notes_with_name( $name ) {
		global $wpdb;
		return $wpdb->get_col(
			$wpdb->prepare(
				"SELECT note_id FROM {$wpdb->prefix}wc_admin_notes WHERE name = %s ORDER BY note_id ASC",
				$name
			)
		);
	}

	/**
	 * Find the ids of all notes with a given type.
	 *
	 * @param string $note_type Type to search for.
	 * @return array An array of matching note ids.
	 */
	public function get_note_ids_by_type( $note_type ) {
		global $wpdb;
		return $wpdb->get_col(
			$wpdb->prepare(
				"SELECT note_id FROM {$wpdb->prefix}wc_admin_notes WHERE type = %s ORDER BY note_id ASC",
				$note_type
			)
		);
	}
}
Admin/Notes/Note.php000064400000045753152333011040010310 0ustar00<?php
/**
 * WooCommerce Admin (Dashboard) Notes.
 *
 * The WooCommerce admin notes class gets admin notes data from storage and checks validity.
 */

namespace Automattic\WooCommerce\Admin\Notes;

defined( 'ABSPATH' ) || exit;

/**
 * Note class.
 */
class Note extends \WC_Data {

	// Note types.
	const E_WC_ADMIN_NOTE_ERROR         = 'error';     // used for presenting error conditions.
	const E_WC_ADMIN_NOTE_WARNING       = 'warning';   // used for presenting warning conditions.
	const E_WC_ADMIN_NOTE_UPDATE        = 'update';    // i.e. used when a new version is available.
	const E_WC_ADMIN_NOTE_INFORMATIONAL = 'info';      // used for presenting informational messages.
	const E_WC_ADMIN_NOTE_MARKETING     = 'marketing'; // used for adding marketing messages.
	const E_WC_ADMIN_NOTE_SURVEY        = 'survey';    // used for adding survey messages.
	const E_WC_ADMIN_NOTE_EMAIL         = 'email';     // used for adding notes that will be sent by email.

	// Note status codes.
	const E_WC_ADMIN_NOTE_PENDING    = 'pending';    // the note is pending - hidden but not actioned.
	const E_WC_ADMIN_NOTE_UNACTIONED = 'unactioned'; // the note has not yet been actioned by a user.
	const E_WC_ADMIN_NOTE_ACTIONED   = 'actioned';   // the note has had its action completed by a user.
	const E_WC_ADMIN_NOTE_SNOOZED    = 'snoozed';    // the note has been snoozed by a user.
	const E_WC_ADMIN_NOTE_SENT       = 'sent';    // the note has been sent by email to the user.

	/**
	 * This is the name of this object type.
	 *
	 * @var string
	 */
	protected $object_type = 'admin-note';

	/**
	 * Cache group.
	 *
	 * @var string
	 */
	protected $cache_group = 'admin-note';

	/**
	 * Note constructor. Loads note data.
	 *
	 * @param mixed $data Note data, object, or ID.
	 */
	public function __construct( $data = '' ) {
		// Set default data here to allow `content_data` to be an object.
		$this->data = array(
			'name'          => '-',
			'type'          => self::E_WC_ADMIN_NOTE_INFORMATIONAL,
			'locale'        => 'en_US',
			'title'         => '-',
			'content'       => '-',
			'content_data'  => new \stdClass(),
			'status'        => self::E_WC_ADMIN_NOTE_UNACTIONED,
			'source'        => 'woocommerce',
			'date_created'  => '0000-00-00 00:00:00',
			'date_reminder' => '',
			'is_snoozable'  => false,
			'actions'       => array(),
			'layout'        => 'plain',
			'image'         => '',
			'is_deleted'    => false,
			'is_read'       => false,
		);

		parent::__construct( $data );

		if ( $data instanceof Note ) {
			$this->set_id( absint( $data->get_id() ) );
		} elseif ( is_numeric( $data ) ) {
			$this->set_id( $data );
		} elseif ( is_object( $data ) && ! empty( $data->note_id ) ) {
			$this->set_id( $data->note_id );
			unset( $data->icon ); // Icons are deprecated.
			$this->set_props( (array) $data );
			$this->set_object_read( true );
		} else {
			$this->set_object_read( true );
		}

		$this->data_store = Notes::load_data_store();
		if ( $this->get_id() > 0 ) {
			$this->data_store->read( $this );
		}
	}

	/**
	 * Merge changes with data and clear.
	 *
	 * @since 3.0.0
	 */
	public function apply_changes() {
		$this->data = array_replace_recursive( $this->data, $this->changes ); // @codingStandardsIgnoreLine

		// Note actions need to be replaced wholesale.
		// Merging arrays doesn't allow for deleting note actions.
		if ( isset( $this->changes['actions'] ) ) {
			$this->data['actions'] = $this->changes['actions'];
		}

		$this->changes = array();
	}

	/*
	|--------------------------------------------------------------------------
	| Helpers
	|--------------------------------------------------------------------------
	|
	| Methods for getting allowed types, statuses.
	|
	*/

	/**
	 * Get allowed types.
	 *
	 * @return array
	 */
	public static function get_allowed_types() {
		$allowed_types = array(
			self::E_WC_ADMIN_NOTE_ERROR,
			self::E_WC_ADMIN_NOTE_WARNING,
			self::E_WC_ADMIN_NOTE_UPDATE,
			self::E_WC_ADMIN_NOTE_INFORMATIONAL,
			self::E_WC_ADMIN_NOTE_MARKETING,
			self::E_WC_ADMIN_NOTE_SURVEY,
			self::E_WC_ADMIN_NOTE_EMAIL,
		);

		return apply_filters( 'woocommerce_note_types', $allowed_types );
	}

	/**
	 * Get allowed statuses.
	 *
	 * @return array
	 */
	public static function get_allowed_statuses() {
		$allowed_statuses = array(
			self::E_WC_ADMIN_NOTE_PENDING,
			self::E_WC_ADMIN_NOTE_ACTIONED,
			self::E_WC_ADMIN_NOTE_UNACTIONED,
			self::E_WC_ADMIN_NOTE_SNOOZED,
			self::E_WC_ADMIN_NOTE_SENT,
		);

		return apply_filters( 'woocommerce_note_statuses', $allowed_statuses );
	}


	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	|
	| Methods for getting data from the note object.
	|
	*/

	/**
	 * Returns all data for this object.
	 *
	 * Override \WC_Data::get_data() to avoid errantly including meta data
	 * from ID collisions with the posts table.
	 *
	 * @return array
	 */
	public function get_data() {
		return array_merge( array( 'id' => $this->get_id() ), $this->data );
	}

	/**
	 * Get note name.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_name( $context = 'view' ) {
		return $this->get_prop( 'name', $context );
	}

	/**
	 * Get note type.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_type( $context = 'view' ) {
		return $this->get_prop( 'type', $context );
	}

	/**
	 * Get note locale.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_locale( $context = 'view' ) {
		return $this->get_prop( 'locale', $context );
	}

	/**
	 * Get note title.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_title( $context = 'view' ) {
		return $this->get_prop( 'title', $context );
	}

	/**
	 * Get note content.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_content( $context = 'view' ) {
		return $this->get_prop( 'content', $context );
	}

	/**
	 * Get note content data (i.e. values that would be needed for re-localization)
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return object
	 */
	public function get_content_data( $context = 'view' ) {
		return $this->get_prop( 'content_data', $context );
	}

	/**
	 * Get note status.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_status( $context = 'view' ) {
		return $this->get_prop( 'status', $context );
	}

	/**
	 * Get note source.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_source( $context = 'view' ) {
		return $this->get_prop( 'source', $context );
	}

	/**
	 * Get date note was created.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_created( $context = 'view' ) {
		return $this->get_prop( 'date_created', $context );
	}

	/**
	 * Get date on which user should be reminded of the note (if any).
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_reminder( $context = 'view' ) {
		return $this->get_prop( 'date_reminder', $context );
	}

	/**
	 * Get note snoozability.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return bool   Whether or not the note can be snoozed.
	 */
	public function get_is_snoozable( $context = 'view' ) {
		return $this->get_prop( 'is_snoozable', $context );
	}

	/**
	 * Get actions on the note (if any).
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_actions( $context = 'view' ) {
		return $this->get_prop( 'actions', $context );
	}

	/**
	 * Get action by action name on the note.
	 *
	 * @param  string $action_name The action name.
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return object the action.
	 */
	public function get_action( $action_name, $context = 'view' ) {
		$actions = $this->get_prop( 'actions', $context );

		$matching_action = null;
		foreach ( $actions as $i => $action ) {
			if ( $action->name === $action_name ) {
				$matching_action =& $actions[ $i ];
				break;
			}
		}
		return $matching_action;
	}

	/**
	 * Get note layout (the old notes won't have one).
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_layout( $context = 'view' ) {
		return $this->get_prop( 'layout', $context );
	}

	/**
	 * Get note image (if any).
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_image( $context = 'view' ) {
		return $this->get_prop( 'image', $context );
	}

	/**
	 * Get deleted status.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_is_deleted( $context = 'view' ) {
		return $this->get_prop( 'is_deleted', $context );
	}

	/**
	 * Get is_read status.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_is_read( $context = 'view' ) {
		return $this->get_prop( 'is_read', $context );
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	|
	| Methods for setting note data. These should not update anything in the
	| database itself and should only change what is stored in the class
	| object.
	|
	*/

	/**
	 * Set note name.
	 *
	 * @param string $name Note name.
	 */
	public function set_name( $name ) {
		// Don't allow empty names.
		if ( empty( $name ) ) {
			$this->error( 'admin_note_invalid_data', __( 'The admin note name prop cannot be empty.', 'woocommerce' ) );
		}

		$this->set_prop( 'name', $name );
	}

	/**
	 * Set note type.
	 *
	 * @param string $type Note type.
	 */
	public function set_type( $type ) {
		if ( empty( $type ) ) {
			$this->error( 'admin_note_invalid_data', __( 'The admin note type prop cannot be empty.', 'woocommerce' ) );
		}

		if ( ! in_array( $type, self::get_allowed_types(), true ) ) {
			$this->error(
				'admin_note_invalid_data',
				sprintf(
					/* translators: %s: admin note type. */
					__( 'The admin note type prop (%s) is not one of the supported types.', 'woocommerce' ),
					$type
				)
			);
		}

		$this->set_prop( 'type', $type );
	}

	/**
	 * Set note locale.
	 *
	 * @param string $locale Note locale.
	 */
	public function set_locale( $locale ) {
		if ( empty( $locale ) ) {
			$this->error( 'admin_note_invalid_data', __( 'The admin note locale prop cannot be empty.', 'woocommerce' ) );
		}

		$this->set_prop( 'locale', $locale );
	}

	/**
	 * Set note title.
	 *
	 * @param string $title Note title.
	 */
	public function set_title( $title ) {
		if ( empty( $title ) ) {
			$this->error( 'admin_note_invalid_data', __( 'The admin note title prop cannot be empty.', 'woocommerce' ) );
		}

		$this->set_prop( 'title', $title );
	}

	/**
	 * Set note icon (Deprecated).
	 *
	 * @param string $icon Note icon.
	 */
	public function set_icon( $icon ) {
		wc_deprecated_function( 'set_icon', '4.3' );
	}

	/**
	 * Set note content.
	 *
	 * @param string $content Note content.
	 */
	public function set_content( $content ) {
		$allowed_html = array(
			'br'     => array(),
			'em'     => array(),
			'strong' => array(),
			'a'      => array(
				'href'     => true,
				'rel'      => true,
				'name'     => true,
				'target'   => true,
				'download' => array(
					'valueless' => 'y',
				),
			),
			'p'      => array(),
		);

		$content = wp_kses( $content, $allowed_html );

		if ( empty( $content ) ) {
			$this->error( 'admin_note_invalid_data', __( 'The admin note content prop cannot be empty.', 'woocommerce' ) );
		}

		$this->set_prop( 'content', $content );
	}

	/**
	 * Set note data for potential re-localization.
	 *
	 * @todo Set a default empty array? https://github.com/woocommerce/woocommerce-admin/pull/1763#pullrequestreview-212442921.
	 * @param object $content_data Note data.
	 */
	public function set_content_data( $content_data ) {
		$allowed_type = false;

		// Make sure $content_data is stdClass Object or an array.
		if ( ! ( $content_data instanceof \stdClass ) ) {
			$this->error( 'admin_note_invalid_data', __( 'The admin note content_data prop must be an instance of stdClass.', 'woocommerce' ) );
		}

		$this->set_prop( 'content_data', $content_data );
	}

	/**
	 * Set note status.
	 *
	 * @param string $status Note status.
	 */
	public function set_status( $status ) {
		if ( empty( $status ) ) {
			$this->error( 'admin_note_invalid_data', __( 'The admin note status prop cannot be empty.', 'woocommerce' ) );
		}

		if ( ! in_array( $status, self::get_allowed_statuses(), true ) ) {
			$this->error(
				'admin_note_invalid_data',
				sprintf(
					/* translators: %s: admin note status property. */
					__( 'The admin note status prop (%s) is not one of the supported statuses.', 'woocommerce' ),
					$status
				)
			);
		}

		$this->set_prop( 'status', $status );
	}

	/**
	 * Set note source.
	 *
	 * @param string $source Note source.
	 */
	public function set_source( $source ) {
		if ( empty( $source ) ) {
			$this->error( 'admin_note_invalid_data', __( 'The admin note source prop cannot be empty.', 'woocommerce' ) );
		}

		$this->set_prop( 'source', $source );
	}

	/**
	 * Set date note was created. NULL is not allowed
	 *
	 * @param string|integer $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed.
	 */
	public function set_date_created( $date ) {
		if ( empty( $date ) ) {
			$this->error( 'admin_note_invalid_data', __( 'The admin note date prop cannot be empty.', 'woocommerce' ) );
		}

		if ( is_string( $date ) ) {
			$date = wc_string_to_timestamp( $date );
		}
		$this->set_date_prop( 'date_created', $date );
	}

	/**
	 * Set date admin should be reminded of note. NULL IS allowed
	 *
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date.
	 */
	public function set_date_reminder( $date ) {
		if ( is_string( $date ) ) {
			$date = wc_string_to_timestamp( $date );
		}
		$this->set_date_prop( 'date_reminder', $date );
	}

	/**
	 * Set note snoozability.
	 *
	 * @param bool $is_snoozable Whether or not the note can be snoozed.
	 */
	public function set_is_snoozable( $is_snoozable ) {
		return $this->set_prop( 'is_snoozable', $is_snoozable );
	}

	/**
	 * Clear actions from a note.
	 */
	public function clear_actions() {
		$this->set_prop( 'actions', array() );
	}

	/**
	 * Set note layout.
	 *
	 * @param string $layout Note layout.
	 */
	public function set_layout( $layout ) {
		// If we don't receive a layout we will set it by default as "plain".
		if ( empty( $layout ) ) {
			$layout = 'plain';
		}
		$valid_layouts = array( 'banner', 'plain', 'thumbnail' );
		if ( in_array( $layout, $valid_layouts, true ) ) {
			$this->set_prop( 'layout', $layout );
		} else {
			$this->error( 'admin_note_invalid_data', __( 'The admin note layout has a wrong prop value.', 'woocommerce' ) );
		}
	}

	/**
	 * Set note image.
	 *
	 * @param string $image Note image.
	 */
	public function set_image( $image ) {
		$this->set_prop( 'image', $image );
	}

	/**
	 * Set note deleted status. NULL is not allowed
	 *
	 * @param bool $is_deleted Note deleted status.
	 */
	public function set_is_deleted( $is_deleted ) {
		$this->set_prop( 'is_deleted', $is_deleted );
	}

	/**
	 * Set note is_read status. NULL is not allowed
	 *
	 * @param bool $is_read Note is_read status.
	 */
	public function set_is_read( $is_read ) {
		$this->set_prop( 'is_read', $is_read );
	}

	/**
	 * Add an action to the note
	 *
	 * @param string  $name           Action name (not presented to user).
	 * @param string  $label          Action label (presented as button label).
	 * @param string  $url            Action URL, if navigation needed. Optional.
	 * @param string  $status         Status to transition parent Note to upon click. Defaults to 'actioned'.
	 * @param boolean $primary        Deprecated since version 3.4.0.
	 * @param string  $actioned_text The label to display after the note has been actioned but before it is dismissed in the UI.
	 */
	public function add_action(
		$name,
		$label,
		$url = '',
		$status = self::E_WC_ADMIN_NOTE_ACTIONED,
		$primary = false,
		$actioned_text = ''
	) {
		$name          = wc_clean( $name );
		$label         = wc_clean( $label );
		$query         = esc_url_raw( $url );
		$status        = wc_clean( $status );
		$actioned_text = wc_clean( $actioned_text );

		if ( empty( $name ) ) {
			$this->error( 'admin_note_invalid_data', __( 'The admin note action name prop cannot be empty.', 'woocommerce' ) );
		}

		if ( empty( $label ) ) {
			$this->error( 'admin_note_invalid_data', __( 'The admin note action label prop cannot be empty.', 'woocommerce' ) );
		}

		$action = array(
			'name'          => $name,
			'label'         => $label,
			'query'         => $query,
			'status'        => $status,
			'actioned_text' => $actioned_text,
			'nonce_name'    => null,
			'nonce_action'  => null,
		);

		$note_actions   = $this->get_prop( 'actions', 'edit' );
		$note_actions[] = (object) $action;
		$this->set_prop( 'actions', $note_actions );
	}

	/**
	 * Set actions on a note.
	 *
	 * @param array $actions Note actions.
	 */
	public function set_actions( $actions ) {
		$this->set_prop( 'actions', $actions );
	}

	/**
	 * Add a nonce to an existing note action.
	 *
	 * @link https://codex.wordpress.org/WordPress_Nonces
	 *
	 * @param string $note_action_name Name of action to add a nonce to.
	 * @param string $nonce_action The nonce action.
	 * @param string $nonce_name The nonce Name. This is used as the paramater name in the resulting URL for the action.
	 * @return void
	 * @throws \Exception If note name cannot be found.
	 */
	public function add_nonce_to_action( string $note_action_name, string $nonce_action, string $nonce_name ) {
		$actions = $this->get_prop( 'actions', 'edit' );

		$matching_action = null;
		foreach ( $actions as $i => $action ) {
			if ( $action->name === $note_action_name ) {
				$matching_action =& $actions[ $i ];
			}
		}

		if ( empty( $matching_action ) ) {
			throw new \Exception( sprintf( 'Could not find action %s in note %s', $note_action_name, $this->get_name() ) );
		}

		$matching_action->nonce_action = $nonce_action;
		$matching_action->nonce_name   = $nonce_name;

		$this->set_actions( $actions );
	}
}
Admin/Notes/DeprecatedNotes.php000064400000035462152333011040012450 0ustar00<?php
/**
 * Define deprecated classes to support changing the naming convention of
 * admin notes.
 */

namespace Automattic\WooCommerce\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\DeprecatedClassFacade;

// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound

/**
 * WC_Admin_Note.
 *
 * @deprecated since 4.8.0, use Note
 */
class WC_Admin_Note extends DeprecatedClassFacade {
	// These constants must be redeclared as to not break plugins that use them.
	const E_WC_ADMIN_NOTE_ERROR         = Note::E_WC_ADMIN_NOTE_ERROR;
	const E_WC_ADMIN_NOTE_WARNING       = Note::E_WC_ADMIN_NOTE_WARNING;
	const E_WC_ADMIN_NOTE_UPDATE        = Note::E_WC_ADMIN_NOTE_UPDATE;
	const E_WC_ADMIN_NOTE_INFORMATIONAL = Note::E_WC_ADMIN_NOTE_INFORMATIONAL;
	const E_WC_ADMIN_NOTE_MARKETING     = Note::E_WC_ADMIN_NOTE_MARKETING;
	const E_WC_ADMIN_NOTE_SURVEY        = Note::E_WC_ADMIN_NOTE_SURVEY;
	const E_WC_ADMIN_NOTE_PENDING       = Note::E_WC_ADMIN_NOTE_PENDING;
	const E_WC_ADMIN_NOTE_UNACTIONED    = Note::E_WC_ADMIN_NOTE_UNACTIONED;
	const E_WC_ADMIN_NOTE_ACTIONED      = Note::E_WC_ADMIN_NOTE_ACTIONED;
	const E_WC_ADMIN_NOTE_SNOOZED       = Note::E_WC_ADMIN_NOTE_SNOOZED;
	const E_WC_ADMIN_NOTE_EMAIL         = Note::E_WC_ADMIN_NOTE_EMAIL;

	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Admin\Notes\Note';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';

	/**
	 * Note constructor. Loads note data.
	 *
	 * @param mixed $data Note data, object, or ID.
	 */
	public function __construct( $data = '' ) {
		$this->instance = new static::$facade_over_classname( $data );
	}
}

/**
 * WC_Admin_Notes.
 *
 * @deprecated since 4.8.0, use Notes
 */
class WC_Admin_Notes extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Admin\Notes\Notes';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Coupon_Page_Moved.
 *
 * @deprecated since 4.8.0, use CouponPageMoved
 */
class WC_Admin_Notes_Coupon_Page_Moved extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\CouponPageMoved';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Customize_Store_With_Blocks.
 *
 * @deprecated since 4.8.0, use CustomizeStoreWithBlocks
 */
class WC_Admin_Notes_Customize_Store_With_Blocks extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\CustomizeStoreWithBlocks';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Edit_Products_On_The_Move.
 *
 * @deprecated since 4.8.0, use EditProductsOnTheMove
 */
class WC_Admin_Notes_Edit_Products_On_The_Move extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\EditProductsOnTheMove';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_EU_VAT_Number.
 *
 * @deprecated since 4.8.0, use EUVATNumber
 */
class WC_Admin_Notes_EU_VAT_Number extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\EUVATNumber';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Facebook_Marketing_Expert.
 *
 * @deprecated since 4.8.0, use FacebookMarketingExpert
 */
class WC_Admin_Notes_Facebook_Marketing_Expert extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Admin\Notes\FacebookMarketingExpert';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_First_Product.
 *
 * @deprecated since 4.8.0, use FirstProduct
 */
class WC_Admin_Notes_First_Product extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\FirstProduct';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Giving_Feedback_Notes.
 *
 * @deprecated since 4.8.0, use GivingFeedbackNotes
 */
class WC_Admin_Notes_Giving_Feedback_Notes extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\GivingFeedbackNotes';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Install_JP_And_WCS_Plugins.
 *
 * @deprecated since 4.8.0, use InstallJPAndWCSPlugins
 */
class WC_Admin_Notes_Install_JP_And_WCS_Plugins extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Launch_Checklist.
 *
 * @deprecated since 4.8.0, use LaunchChecklist
 */
class WC_Admin_Notes_Launch_Checklist extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\LaunchChecklist';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Migrate_From_Shopify.
 *
 * @deprecated since 4.8.0, use MigrateFromShopify
 */
class WC_Admin_Notes_Migrate_From_Shopify extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\MigrateFromShopify';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Mobile_App.
 *
 * @deprecated since 4.8.0, use MobileApp
 */
class WC_Admin_Notes_Mobile_App extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\MobileApp';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_New_Sales_Record.
 *
 * @deprecated since 4.8.0, use NewSalesRecord
 */
class WC_Admin_Notes_New_Sales_Record extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\NewSalesRecord';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Onboarding_Email_Marketing.
 *
 * @deprecated since 4.8.0, use OnboardingEmailMarketing
 */
class WC_Admin_Notes_Onboarding_Email_Marketing extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Admin\Notes\OnboardingEmailMarketing';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Onboarding_Payments.
 *
 * @deprecated since 4.8.0, use OnboardingPayments
 */
class WC_Admin_Notes_Onboarding_Payments extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\OnboardingPayments';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Online_Clothing_Store.
 *
 * @deprecated since 4.8.0, use OnlineClothingStore
 */
class WC_Admin_Notes_Online_Clothing_Store extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\OnlineClothingStore';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Order_Milestones.
 *
 * @deprecated since 4.8.0, use OrderMilestones
 */
class WC_Admin_Notes_Order_Milestones extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\OrderMilestones';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Performance_On_Mobile.
 *
 * @deprecated since 4.8.0, use PerformanceOnMobile
 */
class WC_Admin_Notes_Performance_On_Mobile extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\PerformanceOnMobile';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Personalize_Store.
 *
 * @deprecated since 4.8.0, use PersonalizeStore
 */
class WC_Admin_Notes_Personalize_Store extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\PersonalizeStore';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Real_Time_Order_Alerts.
 *
 * @deprecated since 4.8.0, use RealTimeOrderAlerts
 */
class WC_Admin_Notes_Real_Time_Order_Alerts extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\RealTimeOrderAlerts';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Selling_Online_Courses.
 *
 * @deprecated since 4.8.0, use SellingOnlineCourses
 */
class WC_Admin_Notes_Selling_Online_Courses extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Test_Checkout.
 *
 * @deprecated since 4.8.0, use TestCheckout
 */
class WC_Admin_Notes_Test_Checkout extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\TestCheckout';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Tracking_Opt_In.
 *
 * @deprecated since 4.8.0, use TrackingOptIn
 */
class WC_Admin_Notes_Tracking_Opt_In extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\TrackingOptIn';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_Woo_Subscriptions_Notes.
 *
 * @deprecated since 4.8.0, use WooSubscriptionsNotes
 */
class WC_Admin_Notes_Woo_Subscriptions_Notes extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\WooSubscriptionsNotes';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_WooCommerce_Payments.
 *
 * @deprecated since 4.8.0, use WooCommercePayments
 */
class WC_Admin_Notes_WooCommerce_Payments extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\WooCommercePayments';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}

/**
 * WC_Admin_Notes_WooCommerce_Subscriptions.
 *
 * @deprecated since 4.8.0, use WooCommerceSubscriptions
 */
class WC_Admin_Notes_WooCommerce_Subscriptions extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\WooCommerceSubscriptions';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '4.8.0';
}
Admin/DeprecatedClassFacade.php000064400000004467152333011040012422 0ustar00<?php
/**
 * A facade to allow deprecating an entire class. Calling instance or static
 * functions on the facade triggers a deprecation notice before calling the
 * underlying function.
 *
 * Use it by extending DeprecatedClassFacade in your facade class, setting the
 * static $facade_over_classname string to the name of the class to build
 * a facade over, and setting the static $deprecated_in_version to the version
 * that the class was deprecated in. Eg.:
 *
 * class DeprecatedGoose extends DeprecatedClassFacade {
 *     static $facade_over_classname = 'Goose';
 *     static $deprecated_in_version = '1.7.0';
 * }
 */

namespace Automattic\WooCommerce\Admin;

defined( 'ABSPATH' ) || exit;

// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped

/**
 * A facade to allow deprecating an entire class.
 */
class DeprecatedClassFacade {
	/**
	 * The instance that this facade covers over.
	 *
	 * @var object
	 */
	protected $instance;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->instance = new static::$facade_over_classname();
	}

	/**
	 * Log a deprecation to the error log.
	 *
	 * @param string $function The name of the deprecated function being called.
	 */
	private static function log_deprecation( $function ) {
		error_log( // phpcs:ignore
			sprintf(
				'%1$s is deprecated since version %2$s! Use %3$s instead.',
				static::class . '::' . $function,
				static::$deprecated_in_version,
				static::$facade_over_classname . '::' . $function
			)
		);
	}

	/**
	 * Executes when calling any function on an instance of this class.
	 *
	 * @param string $name      The name of the function being called.
	 * @param array  $arguments An array of the arguments to the function call.
	 */
	public function __call( $name, $arguments ) {
		self::log_deprecation( $name );

		return call_user_func_array(
			array(
				$this->instance,
				$name,
			),
			$arguments
		);
	}

	/**
	 * Executes when calling any static function on this class.
	 *
	 * @param string $name      The name of the function being called.
	 * @param array  $arguments An array of the arguments to the function call.
	 */
	public static function __callStatic( $name, $arguments ) {
		self::log_deprecation( $name );

		return call_user_func_array(
			array(
				static::$facade_over_classname,
				$name,
			),
			$arguments
		);
	}
}
Admin/API/OnboardingProfile.php000064400000040421152333011040012312 0ustar00<?php
/**
 * REST API Onboarding Profile Controller
 *
 * Handles requests to /onboarding/profile
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile as Profile;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProducts;
use Automattic\Jetpack\Connection\Manager as Jetpack_Connection_Manager;

/**
 * Onboarding Profile controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class OnboardingProfile extends \WC_REST_Data_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'onboarding/profile';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_items' ),
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		// This endpoint is experimental. For internal use only.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/experimental_get_email_prefill',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_email_prefill' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to read onboarding profile data.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check whether a given request has permission to edit onboarding profile data.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Return all onboarding profile data.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-options.php';

		$onboarding_data             = get_option( Profile::DATA_OPTION, array() );
		$onboarding_data['industry'] = isset( $onboarding_data['industry'] ) ? $this->filter_industries( $onboarding_data['industry'] ) : null;
		$item_schema                 = $this->get_item_schema();
		$items                       = array();
		foreach ( $item_schema['properties'] as $key => $property_schema ) {
			$items[ $key ] = isset( $onboarding_data[ $key ] ) ? $onboarding_data[ $key ] : null;
		}

		$wccom_auth               = \WC_Helper_Options::get( 'auth' );
		$items['wccom_connected'] = empty( $wccom_auth['access_token'] ) ? false : true;

		$item = $this->prepare_item_for_response( $items, $request );
		$data = $this->prepare_response_for_collection( $item );

		return rest_ensure_response( $data );
	}

	/**
	 * Filter the industries.
	 *
	 * @param  array $industries list of industries.
	 * @return array
	 */
	protected function filter_industries( $industries ) {
		return apply_filters(
			'woocommerce_admin_onboarding_industries',
			$industries
		);
	}

	/**
	 * Update onboarding profile data.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_items( $request ) {
		$params          = $request->get_json_params();
		$query_args      = $this->prepare_objects_query( $params );
		$onboarding_data = (array) get_option( Profile::DATA_OPTION, array() );
		$profile_data    = array_merge( $onboarding_data, $query_args );
		update_option( Profile::DATA_OPTION, $profile_data );
		do_action( 'woocommerce_onboarding_profile_data_updated', $onboarding_data, $query_args );

		$result = array(
			'status'  => 'success',
			'message' => __( 'Onboarding profile data has been updated.', 'woocommerce' ),
		);

		$response = $this->prepare_item_for_response( $result, $request );
		$data     = $this->prepare_response_for_collection( $response );

		return rest_ensure_response( $data );
	}

	/**
	 * Returns a default email to be pre-filled in OBW. Prioritizes Jetpack if connected,
	 * otherwise will default to WordPress general settings.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_email_prefill( $request ) {
		$result = array(
			'email' => '',
		);

		// Attempt to get email from Jetpack.
		if ( class_exists( Jetpack_Connection_Manager::class ) ) {
			$jetpack_connection_manager = new Jetpack_Connection_Manager();
			if ( $jetpack_connection_manager->is_active() ) {
				$jetpack_user = $jetpack_connection_manager->get_connected_user_data();

				$result['email'] = $jetpack_user['email'];
			}
		}

		// Attempt to get email from WordPress general settings.
		if ( empty( $result['email'] ) ) {
			$result['email'] = get_option( 'admin_email' );
		}

		return rest_ensure_response( $result );
	}

	/**
	 * Prepare objects query.
	 *
	 * @param  array $params The params sent in the request.
	 * @return array
	 */
	protected function prepare_objects_query( $params ) {
		$args       = array();
		$properties = self::get_profile_properties();

		foreach ( $properties as $key => $property ) {
			if ( isset( $params[ $key ] ) ) {
				$args[ $key ] = $params[ $key ];
			}
		}

		/**
		 * Filter the query arguments for a request.
		 *
		 * Enables adding extra arguments or setting defaults for a post
		 * collection request.
		 *
		 * @param array $args    Key value array of query var to query value.
		 * @param array $params The params sent in the request.
		 */
		$args = apply_filters( 'woocommerce_rest_onboarding_profile_object_query', $args, $params );

		return $args;
	}


	/**
	 * Prepare the data object for response.
	 *
	 * @param object          $item Data object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data     = $this->add_additional_fields_to_object( $item, $request );
		$data     = $this->filter_response_by_context( $data, 'view' );
		$response = rest_ensure_response( $data );

		/**
		 * Filter the list returned from the API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param array            $item     The original item.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_onboarding_prepare_profile', $response, $item, $request );
	}

	/**
	 * Get onboarding profile properties.
	 *
	 * @return array
	 */
	public static function get_profile_properties() {
		$properties = array(
			'completed'            => array(
				'type'              => 'boolean',
				'description'       => __( 'Whether or not the profile was completed.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
			),
			'skipped'              => array(
				'type'              => 'boolean',
				'description'       => __( 'Whether or not the profile was skipped.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
			),
			'industry'             => array(
				'type'              => 'array',
				'description'       => __( 'Industry.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
				'items'             => array(
					'type' => 'object',
				),
			),
			'product_types'        => array(
				'type'              => 'array',
				'description'       => __( 'Types of products sold.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'sanitize_callback' => 'wp_parse_slug_list',
				'validate_callback' => 'rest_validate_request_arg',
				'items'             => array(
					'enum' => array_keys( OnboardingProducts::get_allowed_product_types() ),
					'type' => 'string',
				),
			),
			'product_count'        => array(
				'type'              => 'string',
				'description'       => __( 'Number of products to be added.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
				'enum'              => array(
					'0',
					'1-10',
					'11-100',
					'101-1000',
					'1000+',
				),
			),
			'selling_venues'       => array(
				'type'              => 'string',
				'description'       => __( 'Other places the store is selling products.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
				'enum'              => array(
					'no',
					'other',
					'brick-mortar',
					'brick-mortar-other',
					'other-woocommerce',
				),
			),
			'number_employees'     => array(
				'type'              => 'string',
				'description'       => __( 'Number of employees of the store.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
				'enum'              => array(
					'1',
					'<10',
					'10-50',
					'50-250',
					'+250',
					'not specified',
				),
			),
			'revenue'              => array(
				'type'              => 'string',
				'description'       => __( 'Current annual revenue of the store.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
				'enum'              => array(
					'none',
					'up-to-2500',
					'2500-10000',
					'10000-50000',
					'50000-250000',
					'more-than-250000',
					'rather-not-say',
				),
			),
			'other_platform'       => array(
				'type'              => 'string',
				'description'       => __( 'Name of other platform used to sell.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
				'enum'              => array(
					'shopify',
					'bigcommerce',
					'magento',
					'wix',
					'amazon',
					'ebay',
					'etsy',
					'squarespace',
					'other',
				),
			),
			'other_platform_name'  => array(
				'type'              => 'string',
				'description'       => __( 'Name of other platform used to sell (not listed).', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
			),
			'business_extensions'  => array(
				'type'              => 'array',
				'description'       => __( 'Extra business extensions to install.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'sanitize_callback' => 'wp_parse_slug_list',
				'validate_callback' => 'rest_validate_request_arg',
				'items'             => array(
					'enum' => array(
						'jetpack',
						'woocommerce-services',
						'woocommerce-payments',
						'mailchimp-for-woocommerce',
						'creative-mail-by-constant-contact',
						'facebook-for-woocommerce',
						'google-listings-and-ads',
						'mailpoet',
						'tiktok-for-business',
						'tiktok-for-business:alt'
					),
					'type' => 'string',
				),
			),
			'theme'                => array(
				'type'              => 'string',
				'description'       => __( 'Selected store theme.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'sanitize_callback' => 'sanitize_title_with_dashes',
				'validate_callback' => 'rest_validate_request_arg',
			),
			'setup_client'         => array(
				'type'              => 'boolean',
				'description'       => __( 'Whether or not this store was setup for a client.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
			),
			'wccom_connected'      => array(
				'type'              => 'boolean',
				'description'       => __( 'Whether or not the store was connected to WooCommerce.com during the extension flow.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
			),
			'is_agree_marketing'   => array(
				'type'              => 'boolean',
				'description'       => __( 'Whether or not this store agreed to receiving marketing contents from WooCommerce.com.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
			),
			'store_email'          => array(
				'type'              => 'string',
				'description'       => __( 'Store email address.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => array( __CLASS__, 'rest_validate_marketing_email' ),
			),
			'is_store_country_set' => array(
				'type'              => 'boolean',
				'description'       => __( 'Whether or not this store country is set via onboarding profiler.', 'woocommerce' ),
				'context'           => array( 'view' ),
				'readonly'          => true,
				'validate_callback' => 'rest_validate_request_arg',
			),
		);

		return apply_filters( 'woocommerce_rest_onboarding_profile_properties', $properties );
	}

	/**
	 * Optionally validates email if user agreed to marketing or if email is not empty.
	 *
	 * @param mixed           $value Email value.
	 * @param WP_REST_Request $request Request object.
	 * @param string          $param Parameter name.
	 * @return true|WP_Error
	 */
	public static function rest_validate_marketing_email( $value, $request, $param ) {
		$is_agree_marketing = $request->get_param( 'is_agree_marketing' );
		if (
			( $is_agree_marketing || ! empty( $value ) ) &&
			! is_email( $value ) ) {
			return new \WP_Error( 'rest_invalid_email', __( 'Invalid email address', 'woocommerce' ) );
		};
		return true;
	}

	/**
	 * Get the schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		// Unset properties used for collection params.
		$properties = self::get_profile_properties();
		foreach ( $properties as $key => $property ) {
			unset( $properties[ $key ]['default'] );
			unset( $properties[ $key ]['items'] );
			unset( $properties[ $key ]['validate_callback'] );
			unset( $properties[ $key ]['sanitize_callback'] );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'onboarding_profile',
			'type'       => 'object',
			'properties' => $properties,
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		// Unset properties used for item schema.
		$params = self::get_profile_properties();
		foreach ( $params as $key => $param ) {
			unset( $params[ $key ]['context'] );
			unset( $params[ $key ]['readonly'] );
		}

		$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );

		return apply_filters( 'woocommerce_rest_onboarding_profile_collection_params', $params );
	}
}
Admin/API/ProductsLowInStock.php000064400000023012152333011040012464 0ustar00<?php
/**
 * REST API ProductsLowInStock Controller
 *
 * Handles request to /products/low-in-stock
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * ProductsLowInStock controller.
 *
 * @internal
 * @extends WC_REST_Products_Controller
 */
final class ProductsLowInStock extends \WC_REST_Products_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'products/low-in-stock',
			array(
				'args'   => array(),
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get low in stock products.
	 *
	 * @param WP_REST_Request $request request object.
	 *
	 * @return WP_REST_Response|WP_ERROR
	 */
	public function get_items( $request ) {
		$query_results = $this->get_low_in_stock_products(
			$request->get_param( 'page' ),
			$request->get_param( 'per_page' ),
			$request->get_param( 'status' )
		);

		// set images and attributes.
		$query_results['results'] = array_map(
			function( $query_result ) {
				$product                  = wc_get_product( $query_result );
				$query_result->images     = $this->get_images( $product );
				$query_result->attributes = $this->get_attributes( $product );
				return $query_result;
			},
			$query_results['results']
		);

		// set last_order_date.
		$query_results['results'] = $this->set_last_order_date( $query_results['results'] );

		// convert the post data to the expected API response for the backward compatibility.
		$query_results['results'] = array_map( array( $this, 'transform_post_to_api_response' ), $query_results['results'] );

		$response = rest_ensure_response( array_values( $query_results['results'] ) );
		$response->header( 'X-WP-Total', $query_results['total'] );
		$response->header( 'X-WP-TotalPages', $query_results['pages'] );

		return $response;
	}

	/**
	 * Set the last order date for each data.
	 *
	 * @param array $results query result from get_low_in_stock_products.
	 *
	 * @return mixed
	 */
	protected function set_last_order_date( $results = array() ) {
		global $wpdb;
		if ( 0 === count( $results ) ) {
			return $results;
		}

		$wheres = array();
		foreach ( $results as $result ) {
			'product_variation' === $result->post_type ?
				array_push( $wheres, "(product_id={$result->post_parent} and variation_id={$result->ID})" )
				: array_push( $wheres, "product_id={$result->ID}" );
		}

		count( $wheres ) ? $where_clause = implode( ' or ', $wheres ) : $where_clause = $wheres[0];

		$product_lookup_table = $wpdb->prefix . 'wc_order_product_lookup';
		$query_string         = "
			select
				product_id,
				variation_id,
				MAX( wc_order_product_lookup.date_created ) AS last_order_date
			from {$product_lookup_table} wc_order_product_lookup
			where {$where_clause}
			group by product_id
			order by date_created desc
		";

		// phpcs:ignore -- ignore prepare() warning as we're not using any user input here.
		$last_order_dates = $wpdb->get_results( $query_string );
		$last_order_dates_index = array();
		// Make an index with product_id_variation_id as a key
		// so that it can be referenced back without looping the whole array.
		foreach ( $last_order_dates as $last_order_date ) {
			$last_order_dates_index[ $last_order_date->product_id . '_' . $last_order_date->variation_id ] = $last_order_date;
		}

		foreach ( $results as &$result ) {
			'product_variation' === $result->post_type ?
				$index_key   = $result->post_parent . '_' . $result->ID
				: $index_key = $result->ID . '_' . $result->post_parent;

			if ( isset( $last_order_dates_index[ $index_key ] ) ) {
				$result->last_order_date = $last_order_dates_index[ $index_key ]->last_order_date;
			}
		}

		return $results;
	}

	/**
	 * Get low in stock products data.
	 *
	 * @param int    $page current page.
	 * @param int    $per_page items per page.
	 * @param string $status post status.
	 *
	 * @return array
	 */
	protected function get_low_in_stock_products( $page = 1, $per_page = 1, $status = 'publish' ) {
		global $wpdb;

		$offset              = ( $page - 1 ) * $per_page;
		$low_stock_threshold = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );

		$query_string = $this->get_query( $this->is_using_sitewide_stock_threshold_only() );

		$query_results = $wpdb->get_results(
			// phpcs:ignore -- not sure why phpcs complains about this line when prepare() is used here.
			$wpdb->prepare( $query_string, $status, $low_stock_threshold, $offset, $per_page ),
			OBJECT_K
		);

		$total_results = $wpdb->get_var( 'SELECT FOUND_ROWS()' );

		return array(
			'results' => $query_results,
			'total'   => (int) $total_results,
			'pages'   => (int) ceil( $total_results / (int) $per_page ),
		);
	}

	/**
	 * Check to see if store is using sitewide threshold only. Meaning that it does not have any custom
	 * stock threshold for a product.
	 *
	 * @return bool
	 */
	protected function is_using_sitewide_stock_threshold_only() {
		global $wpdb;
		$count = $wpdb->get_var( "select count(*) as total from {$wpdb->postmeta} where meta_key='_low_stock_amount'" );
		return 0 === (int) $count;
	}

	/**
	 * Transform post object to expected API response.
	 *
	 * @param object $query_result a row of query result from get_low_in_stock_products().
	 *
	 * @return array
	 */
	protected function transform_post_to_api_response( $query_result ) {
		$low_stock_amount = null;
		if ( isset( $query_result->low_stock_amount ) ) {
			$low_stock_amount = (int) $query_result->low_stock_amount;
		}

		if ( ! isset( $query_result->last_order_date ) ) {
			$query_result->last_order_date = null;
		}

		return array(
			'id'               => (int) $query_result->ID,
			'images'           => $query_result->images,
			'attributes'       => $query_result->attributes,
			'low_stock_amount' => $low_stock_amount,
			'last_order_date'  => wc_rest_prepare_date_response( $query_result->last_order_date ),
			'name'             => $query_result->post_title,
			'parent_id'        => (int) $query_result->post_parent,
			'stock_quantity'   => (int) $query_result->stock_quantity,
			'type'             => 'product_variation' === $query_result->post_type ? 'variation' : 'simple',
		);
	}

	/**
	 * Generate a query.
	 *
	 * @param bool $siteside_only generates a query for sitewide low stock threshold only query.
	 *
	 * @return string
	 */
	protected function get_query( $siteside_only = false ) {
		global $wpdb;
		$query = "
			SELECT
				SQL_CALC_FOUND_ROWS wp_posts.*,
				:postmeta_select
				wc_product_meta_lookup.stock_quantity
			FROM
			  {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup
			  LEFT JOIN {$wpdb->posts} wp_posts ON wp_posts.ID = wc_product_meta_lookup.product_id
			  :postmeta_join
			WHERE
			  wp_posts.post_type IN ('product', 'product_variation')
			  AND wp_posts.post_status = %s
			  AND wc_product_meta_lookup.stock_quantity IS NOT NULL
			  AND wc_product_meta_lookup.stock_status IN('instock', 'outofstock')
			  :postmeta_wheres
			order by wc_product_meta_lookup.product_id DESC
			limit %d, %d
		";

		$postmeta = array(
			'select' => '',
			'join'   => '',
			'wheres' => 'AND wc_product_meta_lookup.stock_quantity <= %d',
		);

		if ( ! $siteside_only ) {
			$postmeta['select'] = 'meta.meta_value AS low_stock_amount,';
			$postmeta['join']   = "LEFT JOIN {$wpdb->postmeta} AS meta ON wp_posts.ID = meta.post_id
			  AND meta.meta_key = '_low_stock_amount'";
			$postmeta['wheres'] = "AND (
			    (
			      meta.meta_value > ''
			      AND wc_product_meta_lookup.stock_quantity <= CAST(
			        meta.meta_value AS SIGNED
			      )
			    )
			    OR (
			      (
			        meta.meta_value IS NULL
			        OR meta.meta_value <= ''
			      )
			      AND wc_product_meta_lookup.stock_quantity <= %d
			    )
		    )";
		}

		return strtr(
			$query,
			array(
				':postmeta_select' => $postmeta['select'],
				':postmeta_join'   => $postmeta['join'],
				':postmeta_wheres' => $postmeta['wheres'],
			)
		);
	}

	/**
	 * Get the query params for collections of attachments.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                       = array();
		$params['context']            = $this->get_context_param();
		$params['context']['default'] = 'view';

		$params['page']     = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page'] = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['status'] = array(
			'default'           => 'publish',
			'description'       => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ),
			'sanitize_callback' => 'sanitize_key',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
Admin/API/NavigationFavorites.php000064400000011515152333011040012673 0ustar00<?php
/**
 * REST API Navigation Favorites controller
 *
 * Handles requests to the navigation favorites endpoint
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Features\Navigation\Favorites;

/**
 * REST API Favorites controller class.
 *
 * @internal
 * @extends WC_REST_CRUD_Controller
 */
class NavigationFavorites extends \WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'navigation/favorites';

	/**
	 * Error code to status code mapping.
	 *
	 * @var array
	 */
	protected $error_to_status_map = array(
		'woocommerce_favorites_invalid_request' => 400,
		'woocommerce_favorites_already_exists'  => 409,
		'woocommerce_favorites_does_not_exist'  => 404,
		'woocommerce_favorites_invalid_user'    => 400,
		'woocommerce_favorites_unauthenticated' => 401,
	);

	/**
	 * Register the routes
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/me',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'current_user_permissions_check' ),
				),
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'add_item' ),
					'permission_callback' => array( $this, 'current_user_permissions_check' ),
					'args'                => array(
						'item_id' => array(
							'required' => true,
						),
					),
				),
				array(
					'methods'             => \WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'current_user_permissions_check' ),
					'args'                => array(
						'item_id' => array(
							'required' => true,
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

	}

	/**
	 * Get all favorites.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response
	 */
	public function get_items( $request ) {
		$response = Favorites::get_all( get_current_user_id() );

		if ( is_wp_error( $response ) || ! $response ) {
			return rest_ensure_response( $this->prepare_error( $response ) );
		}

		return rest_ensure_response(
			array_map( 'stripslashes', $response )
		);
	}

	/**
	 * Add a favorite.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response
	 */
	public function add_item( $request ) {
		$user_id = get_current_user_id();
		$fav_id  = $request->get_param( 'item_id' );
		$user    = get_userdata( $user_id );

		if ( false === $user ) {
			return $this->prepare_error(
				new \WP_Error(
					'woocommerce_favorites_invalid_user',
					__( 'Invalid user_id provided', 'woocommerce' )
				)
			);
		}

		$response = Favorites::add_item( $fav_id, $user_id );

		if ( is_wp_error( $response ) || ! $response ) {
			return rest_ensure_response( $this->prepare_error( $response ) );
		}

		return rest_ensure_response( Favorites::get_all( $user_id ) );
	}

	/**
	 * Delete a favorite.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response
	 */
	public function delete_item( $request ) {
		$user_id = get_current_user_id();
		$fav_id  = $request->get_param( 'item_id' );

		$response = Favorites::remove_item( $fav_id, $user_id );

		if ( is_wp_error( $response ) || ! $response ) {
			return rest_ensure_response( $this->prepare_error( $response ) );
		}

		return rest_ensure_response( Favorites::get_all( $user_id ) );
	}

	/**
	 * Check whether a given request has permission to create favorites.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function add_item_permissions_check( $request ) {
		return current_user_can( 'edit_users' );
	}

	/**
	 * Check whether a given request has permission to delete notes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function delete_item_permissions_check( $request ) {
		return current_user_can( 'edit_users' );
	}

	/**
	 * Always allow for operations that only impact current user
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function current_user_permissions_check( $request ) {
		return true;
	}

	/**
	 * Accept an instance of WP_Error and add the appropriate data for REST transit.
	 *
	 * @param  WP_Error $error Error to prepare.
	 * @return WP_Error
	 */
	protected function prepare_error( $error ) {
		if ( ! is_wp_error( $error ) ) {
			return $error;
		}

		$error->add_data(
			array(
				'status' => $this->error_to_status_map[ $error->get_error_code() ] ?? 500,
			)
		);

		return $error;
	}

}
Admin/API/Orders.php000064400000022301152333011040010142 0ustar00<?php
/**
 * REST API Orders Controller
 *
 * Handles requests to /orders/*
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;

/**
 * Orders controller.
 *
 * @internal
 * @extends WC_REST_Orders_Controller
 */
class Orders extends \WC_REST_Orders_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();
		// This needs to remain a string to support extensions that filter Order Number.
		$params['number'] = array(
			'description'       => __( 'Limit result set to orders matching part of an order number.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		// Fix the default 'status' value until it can be patched in core.
		$params['status']['default'] = array( 'any' );

		// Analytics settings may affect the allowed status list.
		$params['status']['items']['enum'] = ReportsController::get_order_statuses();

		return $params;
	}

	/**
	 * Prepare objects query.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		global $wpdb;
		$args = parent::prepare_objects_query( $request );

		// Search by partial order number.
		if ( ! empty( $request['number'] ) ) {
			$partial_number = trim( $request['number'] );
			$limit          = intval( $args['posts_per_page'] );
			$order_ids      = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT ID
					FROM {$wpdb->prefix}posts
					WHERE post_type = 'shop_order'
					AND ID LIKE %s
					LIMIT %d",
					$wpdb->esc_like( absint( $partial_number ) ) . '%',
					$limit
				)
			);

			// Force WP_Query return empty if don't found any order.
			$order_ids        = empty( $order_ids ) ? array( 0 ) : $order_ids;
			$args['post__in'] = $order_ids;
		}

		return $args;
	}

	/**
	 * Get product IDs, names, and quantity from order ID.
	 *
	 * @param array $order_id ID of order.
	 * @return array
	 */
	protected function get_products_by_order_id( $order_id ) {
		global $wpdb;
		$order_items_table    = $wpdb->prefix . 'woocommerce_order_items';
		$order_itemmeta_table = $wpdb->prefix . 'woocommerce_order_itemmeta';
		$products             = $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT
				order_id,
				order_itemmeta.meta_value as product_id,
				order_itemmeta_2.meta_value as product_quantity,
				order_itemmeta_3.meta_value as variation_id,
				{$wpdb->posts}.post_title as product_name
			FROM {$order_items_table} order_items
			    LEFT JOIN {$order_itemmeta_table} order_itemmeta on order_items.order_item_id = order_itemmeta.order_item_id
			    LEFT JOIN {$order_itemmeta_table} order_itemmeta_2 on order_items.order_item_id = order_itemmeta_2.order_item_id
			    LEFT JOIN {$order_itemmeta_table} order_itemmeta_3 on order_items.order_item_id = order_itemmeta_3.order_item_id
			    LEFT JOIN {$wpdb->posts} on {$wpdb->posts}.ID = order_itemmeta.meta_value
			WHERE
				order_id = ( %d )
			    AND order_itemmeta.meta_key = '_product_id'
				AND order_itemmeta_2.meta_key = '_qty'
			  	AND order_itemmeta_3.meta_key = '_variation_id'
			GROUP BY product_id
			", // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$order_id
			),
			ARRAY_A
		);

		return $products;
	}

	/**
	 * Get customer data from customer_id.
	 *
	 * @param array $customer_id ID of customer.
	 * @return array
	 */
	protected function get_customer_by_id( $customer_id ) {
		global $wpdb;

		$customer_lookup_table = $wpdb->prefix . 'wc_customer_lookup';

		$customer = $wpdb->get_row(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT * FROM {$customer_lookup_table} WHERE customer_id = ( %d )",
				$customer_id
			),
			ARRAY_A
		);

		return $customer;
	}

	/**
	 * Get formatted item data.
	 *
	 * @param  WC_Data $object WC_Data instance.
	 * @return array
	 */
	protected function get_formatted_item_data( $object ) {
		$extra_fields = array( 'customer', 'products' );
		$fields       = false;
		// Determine if the response fields were specified.
		if ( ! empty( $this->request['_fields'] ) ) {
			$fields = wp_parse_list( $this->request['_fields'] );

			if ( 0 === count( $fields ) ) {
				$fields = false;
			} else {
				$fields = array_map( 'trim', $fields );
			}
		}

		// Initially skip line items if we can.
		$using_order_class_override = is_a( $object, '\Automattic\WooCommerce\Admin\Overrides\Order' );
		if ( $using_order_class_override ) {
			$data = $object->get_data_without_line_items();
		} else {
			$data = $object->get_data();
		}

		$extra_fields      = false === $fields ? array() : array_intersect( $extra_fields, $fields );
		$format_decimal    = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' );
		$format_date       = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' );
		$format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' );

		// Add extra data as necessary.
		$extra_data = array();
		foreach ( $extra_fields as $field ) {
			switch ( $field ) {
				case 'customer':
					$extra_data['customer'] = $this->get_customer_by_id( $data['customer_id'] );
					break;
				case 'products':
					$extra_data['products'] = $this->get_products_by_order_id( $object->get_id() );
					break;
			}
		}
		// Format decimal values.
		foreach ( $format_decimal as $key ) {
			$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
		}

		// format total with order currency.
		if ( $object instanceof \WC_Order ) {
			$data['total_formatted'] = wp_strip_all_tags( html_entity_decode( $object->get_formatted_order_total() ), true );
		}

		// Format date values.
		foreach ( $format_date as $key ) {
			$datetime              = $data[ $key ];
			$data[ $key ]          = wc_rest_prepare_date_response( $datetime, false );
			$data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime );
		}

		// Format the order status.
		$data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];

		// Format requested line items.
		$formatted_line_items = array();

		foreach ( $format_line_items as $key ) {
			if ( false === $fields || in_array( $key, $fields, true ) ) {
				if ( $using_order_class_override ) {
					$line_item_data = $object->get_line_item_data( $key );
				} else {
					$line_item_data = $data[ $key ];
				}
				$formatted_line_items[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $line_item_data ) );
			}
		}

		// Refunds.
		$data['refunds'] = array();
		foreach ( $object->get_refunds() as $refund ) {
			$data['refunds'][] = array(
				'id'     => $refund->get_id(),
				'reason' => $refund->get_reason() ? $refund->get_reason() : '',
				'total'  => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ),
			);
		}

		return array_merge(
			array(
				'id'                   => $object->get_id(),
				'parent_id'            => $data['parent_id'],
				'number'               => $data['number'],
				'order_key'            => $data['order_key'],
				'created_via'          => $data['created_via'],
				'version'              => $data['version'],
				'status'               => $data['status'],
				'currency'             => $data['currency'],
				'date_created'         => $data['date_created'],
				'date_created_gmt'     => $data['date_created_gmt'],
				'date_modified'        => $data['date_modified'],
				'date_modified_gmt'    => $data['date_modified_gmt'],
				'discount_total'       => $data['discount_total'],
				'discount_tax'         => $data['discount_tax'],
				'shipping_total'       => $data['shipping_total'],
				'shipping_tax'         => $data['shipping_tax'],
				'cart_tax'             => $data['cart_tax'],
				'total'                => $data['total'],
				'total_formatted'      => isset( $data['total_formatted'] ) ? $data['total_formatted'] : $data['total'],
				'total_tax'            => $data['total_tax'],
				'prices_include_tax'   => $data['prices_include_tax'],
				'customer_id'          => $data['customer_id'],
				'customer_ip_address'  => $data['customer_ip_address'],
				'customer_user_agent'  => $data['customer_user_agent'],
				'customer_note'        => $data['customer_note'],
				'billing'              => $data['billing'],
				'shipping'             => $data['shipping'],
				'payment_method'       => $data['payment_method'],
				'payment_method_title' => $data['payment_method_title'],
				'transaction_id'       => $data['transaction_id'],
				'date_paid'            => $data['date_paid'],
				'date_paid_gmt'        => $data['date_paid_gmt'],
				'date_completed'       => $data['date_completed'],
				'date_completed_gmt'   => $data['date_completed_gmt'],
				'cart_hash'            => $data['cart_hash'],
				'meta_data'            => $data['meta_data'],
				'refunds'              => $data['refunds'],
			),
			$formatted_line_items,
			$extra_data
		);
	}
}
Admin/API/Coupons.php000064400000004232152333011040010335 0ustar00<?php
/**
 * REST API Coupons Controller
 *
 * Handles requests to /coupons/*
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Coupons controller.
 *
 * @internal
 * @extends WC_REST_Coupons_Controller
 */
class Coupons extends \WC_REST_Coupons_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params           = parent::get_collection_params();
		$params['search'] = array(
			'description'       => __( 'Limit results to coupons with codes matching a given string.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		return $params;
	}


	/**
	 * Add coupon code searching to the WC API.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args = parent::prepare_objects_query( $request );

		if ( ! empty( $request['search'] ) ) {
			$args['search'] = $request['search'];
			$args['s']      = false;
		}

		return $args;
	}

	/**
	 * Get a collection of posts and add the code search option to WP_Query.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		add_filter( 'posts_where', array( __CLASS__, 'add_wp_query_search_code_filter' ), 10, 2 );
		$response = parent::get_items( $request );
		remove_filter( 'posts_where', array( __CLASS__, 'add_wp_query_search_code_filter' ), 10 );
		return $response;
	}

	/**
	 * Add code searching to the WP Query
	 *
	 * @internal
	 * @param string $where Where clause used to search posts.
	 * @param object $wp_query WP_Query object.
	 * @return string
	 */
	public static function add_wp_query_search_code_filter( $where, $wp_query ) {
		global $wpdb;

		$search = $wp_query->get( 'search' );
		if ( $search ) {
			$code_like = '%' . $wpdb->esc_like( $search ) . '%';
			$where    .= $wpdb->prepare( "AND {$wpdb->posts}.post_title LIKE %s", $code_like );
		}

		return $where;
	}
}
Admin/API/OnboardingProductTypes.php000064400000003460152333011040013361 0ustar00<?php
/**
 * REST API Onboarding Product Types Controller
 *
 * Handles requests to /onboarding/product-types
 */

namespace Automattic\WooCommerce\Admin\API;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProducts;

defined( 'ABSPATH' ) || exit;

/**
 * Onboarding Product Types Controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class OnboardingProductTypes extends \WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'onboarding/product-types';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_product_types' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to read onboarding profile data.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Return available product types.
	 *
	 * @param \WP_REST_Request $request Request data.
	 *
	 * @return \WP_Error|\WP_REST_Response
	 */
	public function get_product_types( $request ) {
		return OnboardingProducts::get_product_types_with_data();
	}

}
Admin/API/Products.php000064400000023315152333011040010515 0ustar00<?php
/**
 * REST API Products Controller
 *
 * Handles requests to /products/*
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Products controller.
 *
 * @internal
 * @extends WC_REST_Products_Controller
 */
class Products extends \WC_REST_Products_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Local cache of last order dates by ID.
	 *
	 * @var array
	 */
	protected $last_order_dates = array();

	/**
	 * Adds properties that can be embed via ?_embed=1.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = parent::get_item_schema();

		$properties_to_embed = array(
			'id',
			'name',
			'slug',
			'permalink',
			'images',
			'description',
			'short_description',
		);

		foreach ( $properties_to_embed as $property ) {
			$schema['properties'][ $property ]['context'][] = 'embed';
		}

		$schema['properties']['last_order_date'] = array(
			'description' => __( "The date the last order for this product was placed, in the site's timezone.", 'woocommerce' ),
			'type'        => 'date-time',
			'context'     => array( 'view', 'edit' ),
			'readonly'    => true,
		);

		return $schema;
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                 = parent::get_collection_params();
		$params['low_in_stock'] = array(
			'description'       => __( 'Limit result set to products that are low or out of stock. (Deprecated)', 'woocommerce' ),
			'type'              => 'boolean',
			'default'           => false,
			'sanitize_callback' => 'wc_string_to_bool',
		);
		$params['search']       = array(
			'description'       => __( 'Search by similar product name or sku.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		return $params;
	}


	/**
	 * Add product name and sku filtering to the WC API.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args = parent::prepare_objects_query( $request );

		if ( ! empty( $request['search'] ) ) {
			$args['search'] = trim( $request['search'] );
			unset( $args['s'] );
		}
		if ( ! empty( $request['low_in_stock'] ) ) {
			$args['low_in_stock'] = $request['low_in_stock'];
			$args['post_type']    = array( 'product', 'product_variation' );
		}

		return $args;
	}

	/**
	 * Get a collection of posts and add the post title filter option to WP_Query.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		add_filter( 'posts_fields', array( __CLASS__, 'add_wp_query_fields' ), 10, 2 );
		add_filter( 'posts_where', array( __CLASS__, 'add_wp_query_filter' ), 10, 2 );
		add_filter( 'posts_join', array( __CLASS__, 'add_wp_query_join' ), 10, 2 );
		add_filter( 'posts_groupby', array( __CLASS__, 'add_wp_query_group_by' ), 10, 2 );
		$response = parent::get_items( $request );
		remove_filter( 'posts_fields', array( __CLASS__, 'add_wp_query_fields' ), 10 );
		remove_filter( 'posts_where', array( __CLASS__, 'add_wp_query_filter' ), 10 );
		remove_filter( 'posts_join', array( __CLASS__, 'add_wp_query_join' ), 10 );
		remove_filter( 'posts_groupby', array( __CLASS__, 'add_wp_query_group_by' ), 10 );

		/**
		 * The low stock query caused performance issues in WooCommerce 5.5.1
		 * due to a) being slow, and b) multiple requests being made to this endpoint
		 * from WC Admin.
		 *
		 * This is a temporary measure to trigger the user’s browser to cache the
		 * endpoint response for 1 minute, limiting the amount of requests overall.
		 *
		 * https://github.com/woocommerce/woocommerce-admin/issues/7358
		 */
		if ( $this->is_low_in_stock_request( $request ) ) {
			$response->header( 'Cache-Control', 'max-age=300' );
		}
		return $response;
	}

	/**
	 * Check whether the request is for products low in stock.
	 *
	 * It matches requests with paramaters:
	 *
	 * low_in_stock = true
	 * page = 1
	 * fields[0] = id
	 *
	 * @param string $request WP REST API request.
	 * @return boolean Whether the request matches.
	 */
	private function is_low_in_stock_request( $request ) {
		if (
			$request->get_param( 'low_in_stock' ) === true &&
			$request->get_param( 'page' ) === 1 &&
			is_array( $request->get_param( '_fields' ) ) &&
			count( $request->get_param( '_fields' ) ) === 1 &&
			in_array( 'id', $request->get_param( '_fields' ), true )
		) {
			return true;
		}

		return false;
	}

	/**
	 * Hang onto last order date since it will get removed by wc_get_product().
	 *
	 * @param stdClass $object_data Single row from query results.
	 * @return WC_Data
	 */
	public function get_object( $object_data ) {
		if ( isset( $object_data->last_order_date ) ) {
			$this->last_order_dates[ $object_data->ID ] = $object_data->last_order_date;
		}
		return parent::get_object( $object_data );
	}

	/**
	 * Add `low_stock_amount` property to product data
	 *
	 * @param WC_Data         $object  Object data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_object_for_response( $object, $request ) {
		$data        = parent::prepare_object_for_response( $object, $request );
		$object_data = $object->get_data();
		$product_id  = $object_data['id'];

		if ( $request->get_param( 'low_in_stock' ) ) {
			if ( is_numeric( $object_data['low_stock_amount'] ) ) {
				$data->data['low_stock_amount'] = $object_data['low_stock_amount'];
			}
			if ( isset( $this->last_order_dates[ $product_id ] ) ) {
				$data->data['last_order_date'] = wc_rest_prepare_date_response( $this->last_order_dates[ $product_id ] );
			}
		}
		if ( isset( $data->data['name'] ) ) {
			$data->data['name'] = wp_strip_all_tags( $data->data['name'] );
		}

		return $data;
	}

	/**
	 * Add in conditional select fields to the query.
	 *
	 * @internal
	 * @param string $select Select clause used to select fields from the query.
	 * @param object $wp_query WP_Query object.
	 * @return string
	 */
	public static function add_wp_query_fields( $select, $wp_query ) {
		if ( $wp_query->get( 'low_in_stock' ) ) {
			$fields  = array(
				'low_stock_amount_meta.meta_value AS low_stock_amount',
				'MAX( product_lookup.date_created ) AS last_order_date',
			);
			$select .= ', ' . implode( ', ', $fields );
		}

		return $select;
	}

	/**
	 * Add in conditional search filters for products.
	 *
	 * @internal
	 * @param string $where Where clause used to search posts.
	 * @param object $wp_query WP_Query object.
	 * @return string
	 */
	public static function add_wp_query_filter( $where, $wp_query ) {
		global $wpdb;

		$search = $wp_query->get( 'search' );
		if ( $search ) {
			$title_like = '%' . $wpdb->esc_like( $search ) . '%';
			$where     .= $wpdb->prepare( " AND ({$wpdb->posts}.post_title LIKE %s", $title_like );  // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$where     .= wc_product_sku_enabled() ? $wpdb->prepare( ' OR wc_product_meta_lookup.sku LIKE %s)', $search ) : ')';
		}

		if ( $wp_query->get( 'low_in_stock' ) ) {
			$low_stock_amount = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
			$where           .= "
			AND wc_product_meta_lookup.stock_quantity IS NOT NULL
			AND wc_product_meta_lookup.stock_status IN('instock','outofstock')
			AND (
				(
					low_stock_amount_meta.meta_value > ''
					AND wc_product_meta_lookup.stock_quantity <= CAST(low_stock_amount_meta.meta_value AS SIGNED)
				)
				OR (
					(
						low_stock_amount_meta.meta_value IS NULL OR low_stock_amount_meta.meta_value <= ''
					)
					AND wc_product_meta_lookup.stock_quantity <= {$low_stock_amount}
				)
			)";
		}

		return $where;
	}

	/**
	 * Join posts meta tables when product search or low stock query is present.
	 *
	 * @internal
	 * @param string $join Join clause used to search posts.
	 * @param object $wp_query WP_Query object.
	 * @return string
	 */
	public static function add_wp_query_join( $join, $wp_query ) {
		global $wpdb;

		$search = $wp_query->get( 'search' );
		if ( $search && wc_product_sku_enabled() ) {
			$join = self::append_product_sorting_table_join( $join );
		}

		if ( $wp_query->get( 'low_in_stock' ) ) {
			$product_lookup_table = $wpdb->prefix . 'wc_order_product_lookup';

			$join  = self::append_product_sorting_table_join( $join );
			$join .= " LEFT JOIN {$wpdb->postmeta} AS low_stock_amount_meta ON {$wpdb->posts}.ID = low_stock_amount_meta.post_id AND low_stock_amount_meta.meta_key = '_low_stock_amount' ";
			$join .= " LEFT JOIN {$product_lookup_table} product_lookup ON {$wpdb->posts}.ID = CASE
				WHEN {$wpdb->posts}.post_type = 'product' THEN product_lookup.product_id
				WHEN {$wpdb->posts}.post_type = 'product_variation' THEN product_lookup.variation_id
			END";
		}

		return $join;
	}

	/**
	 * Join wc_product_meta_lookup to posts if not already joined.
	 *
	 * @internal
	 * @param string $sql SQL join.
	 * @return string
	 */
	protected static function append_product_sorting_table_join( $sql ) {
		global $wpdb;

		if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) {
			$sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
		}
		return $sql;
	}

	/**
	 * Group by post ID to prevent duplicates.
	 *
	 * @internal
	 * @param string $groupby Group by clause used to organize posts.
	 * @param object $wp_query WP_Query object.
	 * @return string
	 */
	public static function add_wp_query_group_by( $groupby, $wp_query ) {
		global $wpdb;

		$search       = $wp_query->get( 'search' );
		$low_in_stock = $wp_query->get( 'low_in_stock' );
		if ( empty( $groupby ) && ( $search || $low_in_stock ) ) {
			$groupby = $wpdb->posts . '.ID';
		}
		return $groupby;
	}
}
Admin/API/Taxes.php000064400000011634152333011040007777 0ustar00<?php
/**
 * REST API Taxes Controller
 *
 * Handles requests to /taxes/*
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Taxes controller.
 *
 * @internal
 * @extends WC_REST_Taxes_Controller
 */
class Taxes extends \WC_REST_Taxes_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params            = parent::get_collection_params();
		$params['search']  = array(
			'description'       => __( 'Search by similar tax code.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['include'] = array(
			'description'       => __( 'Limit result set to items that have the specified rate ID(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
		);
		return $params;
	}

	/**
	 * Get all taxes and allow filtering by tax code.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		global $wpdb;

		$prepared_args           = array();
		$prepared_args['order']  = $request['order'];
		$prepared_args['number'] = $request['per_page'];
		if ( ! empty( $request['offset'] ) ) {
			$prepared_args['offset'] = $request['offset'];
		} else {
			$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
		}
		$orderby_possibles        = array(
			'id'    => 'tax_rate_id',
			'order' => 'tax_rate_order',
		);
		$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
		$prepared_args['class']   = $request['class'];
		$prepared_args['search']  = $request['search'];
		$prepared_args['include'] = $request['include'];

		/**
		 * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API.
		 *
		 * @param array           $prepared_args Array of arguments for $wpdb->get_results().
		 * @param WP_REST_Request $request       The current request.
		 */
		$prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request );

		$query = "
			SELECT *
			FROM {$wpdb->prefix}woocommerce_tax_rates
			WHERE 1 = 1
		";

		// Filter by tax class.
		if ( ! empty( $prepared_args['class'] ) ) {
			$class  = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : '';
			$query .= " AND tax_rate_class = '$class'";
		}

		// Filter by tax code.
		$tax_code_search = $prepared_args['search'];
		if ( $tax_code_search ) {
			$code_like = '%' . $wpdb->esc_like( $tax_code_search ) . '%';
			$query    .= $wpdb->prepare( ' AND CONCAT_WS( "-", NULLIF(tax_rate_country, ""), NULLIF(tax_rate_state, ""), NULLIF(tax_rate_name, ""), NULLIF(tax_rate_priority, "") ) LIKE %s', $code_like );
		}

		// Filter by included tax rate IDs.
		$included_taxes = array_map( 'absint', $prepared_args['include'] );
		if ( ! empty( $included_taxes ) ) {
			$included_taxes = implode( ',', $prepared_args['include'] );
			$query         .= " AND tax_rate_id IN ({$included_taxes})";
		}

		// Order tax rates.
		$order_by = sprintf( ' ORDER BY %s', sanitize_key( $prepared_args['orderby'] ) );

		// Pagination.
		$pagination = sprintf( ' LIMIT %d, %d', $prepared_args['offset'], $prepared_args['number'] );

		// Query taxes.
		$results = $wpdb->get_results( $query . $order_by . $pagination ); // @codingStandardsIgnoreLine.

		$taxes = array();
		foreach ( $results as $tax ) {
			$data    = $this->prepare_item_for_response( $tax, $request );
			$taxes[] = $this->prepare_response_for_collection( $data );
		}

		$response = rest_ensure_response( $taxes );

		// Store pagination values for headers then unset for count query.
		$per_page = (int) $prepared_args['number'];
		$page     = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );

		// Query only for ids.
		$wpdb->get_results( str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ) ); // @codingStandardsIgnoreLine.

		// Calculate totals.
		$total_taxes = (int) $wpdb->num_rows;
		$response->header( 'X-WP-Total', (int) $total_taxes );
		$max_pages = ceil( $total_taxes / $per_page );
		$response->header( 'X-WP-TotalPages', (int) $max_pages );

		$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}
}
Admin/API/ProductCategories.php000064400000000712152333011040012334 0ustar00<?php
/**
 * REST API Product Categories Controller
 *
 * Handles requests to /products/categories.
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Product categories controller.
 *
 * @internal
 * @extends WC_REST_Product_Categories_Controller
 */
class ProductCategories extends \WC_REST_Product_Categories_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';
}
Admin/API/ProductVariations.php000064400000013735152333011040012377 0ustar00<?php
/**
 * REST API Product Variations Controller
 *
 * Handles requests to /products/variations.
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Product variations controller.
 *
 * @internal
 * @extends WC_REST_Product_Variations_Controller
 */
class ProductVariations extends \WC_REST_Product_Variations_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Register the routes for products.
	 */
	public function register_routes() {
		parent::register_routes();

		// Add a route for listing variations without specifying the parent product ID.
		register_rest_route(
			$this->namespace,
			'/variations',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params           = parent::get_collection_params();
		$params['search'] = array(
			'description'       => __( 'Search by similar product name, sku, or attribute value.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		return $params;
	}

	/**
	 * Add in conditional search filters for variations.
	 *
	 * @internal
	 * @param string $where Where clause used to search posts.
	 * @param object $wp_query WP_Query object.
	 * @return string
	 */
	public static function add_wp_query_filter( $where, $wp_query ) {
		global $wpdb;

		$search = $wp_query->get( 'search' );
		if ( $search ) {
			$like       = '%' . $wpdb->esc_like( $search ) . '%';
			$conditions = array(
				$wpdb->prepare( "{$wpdb->posts}.post_title LIKE %s", $like ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$wpdb->prepare( 'attr_search_meta.meta_value LIKE %s', $like ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			);

			if ( wc_product_sku_enabled() ) {
				$conditions[] = $wpdb->prepare( 'wc_product_meta_lookup.sku LIKE %s', $like );
			}

			$where .= ' AND (' . implode( ' OR ', $conditions ) . ')';
		}

		return $where;
	}

	/**
	 * Join posts meta tables when variation search query is present.
	 *
	 * @internal
	 * @param string $join Join clause used to search posts.
	 * @param object $wp_query WP_Query object.
	 * @return string
	 */
	public static function add_wp_query_join( $join, $wp_query ) {
		global $wpdb;

		$search = $wp_query->get( 'search' );
		if ( $search ) {
			$join .= " LEFT JOIN {$wpdb->postmeta} AS attr_search_meta
						ON {$wpdb->posts}.ID = attr_search_meta.post_id
						AND attr_search_meta.meta_key LIKE 'attribute_%' ";
		}

		if ( wc_product_sku_enabled() && ! strstr( $join, 'wc_product_meta_lookup' ) ) {
			$join .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup
						ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
		}

		return $join;
	}

	/**
	 * Add product name and sku filtering to the WC API.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args = parent::prepare_objects_query( $request );

		if ( ! empty( $request['search'] ) ) {
			$args['search'] = $request['search'];
			unset( $args['s'] );
		}

		// Retreive variations without specifying a parent product.
		if ( "/{$this->namespace}/variations" === $request->get_route() ) {
			unset( $args['post_parent'] );
		}

		return $args;
	}

	/**
	 * Get a collection of posts and add the post title filter option to WP_Query.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		add_filter( 'posts_where', array( __CLASS__, 'add_wp_query_filter' ), 10, 2 );
		add_filter( 'posts_join', array( __CLASS__, 'add_wp_query_join' ), 10, 2 );
		add_filter( 'posts_groupby', array( 'Automattic\WooCommerce\Admin\API\Products', 'add_wp_query_group_by' ), 10, 2 );
		$response = parent::get_items( $request );
		remove_filter( 'posts_where', array( __CLASS__, 'add_wp_query_filter' ), 10 );
		remove_filter( 'posts_join', array( __CLASS__, 'add_wp_query_join' ), 10 );
		remove_filter( 'posts_groupby', array( 'Automattic\WooCommerce\Admin\API\Products', 'add_wp_query_group_by' ), 10 );
		return $response;
	}

	/**
	 * Get the Product's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = parent::get_item_schema();

		$schema['properties']['name']      = array(
			'description' => __( 'Product parent name.', 'woocommerce' ),
			'type'        => 'string',
			'context'     => array( 'view', 'edit' ),
		);
		$schema['properties']['type']      = array(
			'description' => __( 'Product type.', 'woocommerce' ),
			'type'        => 'string',
			'default'     => 'variation',
			'enum'        => array( 'variation' ),
			'context'     => array( 'view', 'edit' ),
		);
		$schema['properties']['parent_id'] = array(
			'description' => __( 'Product parent ID.', 'woocommerce' ),
			'type'        => 'integer',
			'context'     => array( 'view', 'edit' ),
		);

		return $schema;
	}

	/**
	 * Prepare a single variation output for response.
	 *
	 * @param  WC_Data         $object  Object data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_object_for_response( $object, $request ) {
		$context  = empty( $request['context'] ) ? 'view' : $request['context'];
		$response = parent::prepare_object_for_response( $object, $request );
		$data     = $response->get_data();

		$data['name']      = $object->get_name( $context );
		$data['type']      = $object->get_type();
		$data['parent_id'] = $object->get_parent_id( $context );

		$response->set_data( $data );

		return $response;
	}
}
Admin/API/OnboardingThemes.php000064400000014007152333011040012140 0ustar00<?php
/**
 * REST API Onboarding Themes Controller
 *
 * Handles requests to install and activate themes.
 */

namespace Automattic\WooCommerce\Admin\API;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingThemes as Themes;

defined( 'ABSPATH' ) || exit;

/**
 * Onboarding Themes Controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class OnboardingThemes extends \WC_REST_Data_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'onboarding/themes';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/install',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'install_theme' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/activate',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'activate_theme' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);
	}

	/**
	 * Check if a given request has access to manage themes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_item_permissions_check( $request ) {
		if ( ! current_user_can( 'switch_themes' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage themes.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Installs the requested theme.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|array Theme installation status.
	 */
	public function install_theme( $request ) {
		$allowed_themes = Themes::get_allowed_themes();
		$theme          = sanitize_text_field( $request['theme'] );

		if ( ! in_array( $theme, $allowed_themes, true ) ) {
			return new \WP_Error( 'woocommerce_rest_invalid_theme', __( 'Invalid theme.', 'woocommerce' ), 404 );
		}

		$installed_themes = wp_get_themes();

		if ( in_array( $theme, array_keys( $installed_themes ), true ) ) {
			return( array(
				'slug'   => $theme,
				'name'   => $installed_themes[ $theme ]->get( 'Name' ),
				'status' => 'success',
			) );
		}

		include_once ABSPATH . '/wp-admin/includes/admin.php';
		include_once ABSPATH . '/wp-admin/includes/theme-install.php';
		include_once ABSPATH . '/wp-admin/includes/theme.php';
		include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
		include_once ABSPATH . '/wp-admin/includes/class-theme-upgrader.php';

		$api = themes_api(
			'theme_information',
			array(
				'slug'   => $theme,
				'fields' => array(
					'sections' => false,
				),
			)
		);

		if ( is_wp_error( $api ) ) {
			return new \WP_Error(
				'woocommerce_rest_theme_install',
				sprintf(
					/* translators: %s: theme slug (example: woocommerce-services) */
					__( 'The requested theme `%s` could not be installed. Theme API call failed.', 'woocommerce' ),
					$theme
				),
				500
			);
		}

		$upgrader = new \Theme_Upgrader( new \Automatic_Upgrader_Skin() );
		$result   = $upgrader->install( $api->download_link );

		if ( is_wp_error( $result ) || is_null( $result ) ) {
			return new \WP_Error(
				'woocommerce_rest_theme_install',
				sprintf(
					/* translators: %s: theme slug (example: woocommerce-services) */
					__( 'The requested theme `%s` could not be installed.', 'woocommerce' ),
					$theme
				),
				500
			);
		}

		return array(
			'slug'   => $theme,
			'name'   => $api->name,
			'status' => 'success',
		);
	}

	/**
	 * Activate the requested theme.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|array Theme activation status.
	 */
	public function activate_theme( $request ) {
		$allowed_themes = Themes::get_allowed_themes();
		$theme          = sanitize_text_field( $request['theme'] );
		if ( ! in_array( $theme, $allowed_themes, true ) ) {
			return new \WP_Error( 'woocommerce_rest_invalid_theme', __( 'Invalid theme.', 'woocommerce' ), 404 );
		}

		require_once ABSPATH . 'wp-admin/includes/theme.php';

		$installed_themes = wp_get_themes();

		if ( ! in_array( $theme, array_keys( $installed_themes ), true ) ) {
			/* translators: %s: theme slug (example: woocommerce-services) */
			return new \WP_Error( 'woocommerce_rest_invalid_theme', sprintf( __( 'Invalid theme %s.', 'woocommerce' ), $theme ), 404 );
		}

		$result = switch_theme( $theme );
		if ( ! is_null( $result ) ) {
			return new \WP_Error( 'woocommerce_rest_invalid_theme', sprintf( __( 'The requested theme could not be activated.', 'woocommerce' ), $theme ), 500 );
		}

		return( array(
			'slug'   => $theme,
			'name'   => $installed_themes[ $theme ]->get( 'Name' ),
			'status' => 'success',
		) );
	}

	/**
	 * Get the schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'onboarding_theme',
			'type'       => 'object',
			'properties' => array(
				'slug'   => array(
					'description' => __( 'Theme slug.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'   => array(
					'description' => __( 'Theme name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'status' => array(
					'description' => __( 'Theme status.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
Admin/API/MobileAppMagicLink.php000064400000004143152333011040012337 0ustar00<?php
/**
 * REST API Data countries controller.
 *
 * Handles requests to the /mobile-app endpoint.
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

use Automattic\Jetpack\Connection\Manager as Jetpack_Connection_Manager;

/**
 * REST API Data countries controller class.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class MobileAppMagicLink extends \WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'mobile-app';

	/**
	 * Register routes.
	 *
	 * @since 7.0.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/send-magic-link',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'send_magic_link' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		parent::register_routes();
	}

	/**
	 * Sends request to generate magic link email.
	 *
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function send_magic_link() {
		// Attempt to get email from Jetpack.
		if ( class_exists( Jetpack_Connection_Manager::class ) ) {
			$jetpack_connection_manager = new Jetpack_Connection_Manager();
			if ( $jetpack_connection_manager->is_active() ) {
				if ( class_exists( 'Jetpack_IXR_Client' ) ) {
					$xml = new \Jetpack_IXR_Client(
						array(
							'user_id' => get_current_user_id(),
						)
					);

					$xml->query( 'jetpack.sendMobileMagicLink', array( 'app' => 'woocommerce' ) );
					if ( $xml->isError() ) {
						return new \WP_Error(
							'error_sending_mobile_magic_link',
							sprintf(
								'%s: %s',
								$xml->getErrorCode(),
								$xml->getErrorMessage()
							)
						);
					}

					return rest_ensure_response(
						array(
							'code' => 'success',
						)
					);
				}
			}
		}

		return new \WP_Error( 'jetpack_not_connected', __( 'Jetpack is not connected.', 'woocommerce' ) );
	}
}
Admin/API/Marketing.php000064400000007546152333011040010643 0ustar00<?php
/**
 * REST API Marketing Controller
 *
 * Handles requests to /marketing.
 */

namespace Automattic\WooCommerce\Admin\API;

use Automattic\WooCommerce\Internal\Admin\Marketing as MarketingFeature;
use Automattic\WooCommerce\Admin\PluginsHelper;

defined( 'ABSPATH' ) || exit;

/**
 * Marketing Controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class Marketing extends \WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'marketing';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/recommended',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_recommended_plugins' ),
					'permission_callback' => array( $this, 'get_recommended_plugins_permissions_check' ),
					'args'                => array(
						'per_page' => $this->get_collection_params()['per_page'],
						'category' => array(
							'type'              => 'string',
							'validate_callback' => 'rest_validate_request_arg',
							'sanitize_callback' => 'sanitize_title_with_dashes',
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/knowledge-base',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_knowledge_base_posts' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => array(
						'category' => array(
							'type'              => 'string',
							'validate_callback' => 'rest_validate_request_arg',
							'sanitize_callback' => 'sanitize_title_with_dashes',
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to install plugins.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_recommended_plugins_permissions_check( $request ) {
		if ( ! current_user_can( 'install_plugins' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage plugins.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}


	/**
	 * Return installed marketing extensions data.
	 *
	 * @param \WP_REST_Request $request Request data.
	 *
	 * @return \WP_Error|\WP_REST_Response
	 */
	public function get_recommended_plugins( $request ) {
		// Default to marketing category (if no category set).
		$category      = ( ! empty( $request->get_param( 'category' ) ) ) ? $request->get_param( 'category' ) : 'marketing';
		$all_plugins   = MarketingFeature::get_instance()->get_recommended_plugins();
		$valid_plugins = [];
		$per_page      = $request->get_param( 'per_page' );

		foreach ( $all_plugins as $plugin ) {

			// default to marketing if 'categories' is empty on the plugin object (support for legacy api while testing).
			$plugin_categories = ( ! empty( $plugin['categories'] ) ) ? $plugin['categories'] : [ 'marketing' ];

			if ( ! PluginsHelper::is_plugin_installed( $plugin['plugin'] ) && in_array( $category, $plugin_categories, true ) ) {
				$valid_plugins[] = $plugin;
			}
		}

		return rest_ensure_response( array_slice( $valid_plugins, 0, $per_page ) );
	}

	/**
	 * Return installed marketing extensions data.
	 *
	 * @param \WP_REST_Request $request Request data.
	 *
	 * @return \WP_Error|\WP_REST_Response
	 */
	public function get_knowledge_base_posts( $request ) {
		$category = $request->get_param( 'category' );
		return rest_ensure_response( MarketingFeature::get_instance()->get_knowledge_base_posts( $category ) );
	}
}
Admin/API/SettingOptions.php000064400000001557152333011040011707 0ustar00<?php
/**
 * REST API Setting Options Controller
 *
 * Handles requests to /settings/{option}
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;

/**
 * Setting Options controller.
 *
 * @internal
 * @extends WC_REST_Setting_Options_Controller
 */
class SettingOptions extends \WC_REST_Setting_Options_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Invalidates API cache when updating settings options.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array Of WP_Error or WP_REST_Response.
	 */
	public function batch_items( $request ) {
		// Invalidate the API cache.
		ReportsCache::invalidate();

		// Process the request.
		return parent::batch_items( $request );
	}
}
Admin/API/Experiments.php000064400000003510152333011040011210 0ustar00<?php
/**
 * REST API Experiment Controller
 *
 * Handles requests to /experiment
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Data controller.
 *
 * @extends WC_REST_Data_Controller
 */
class Experiments extends \WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'experiments';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/assignment',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_assignment' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}


	/**
	 * Forward the experiment request to WP.com and return the WP.com response.
	 *
	 * @param \WP_REST_Request $request Request data.
	 *
	 * @return \WP_Error|\WP_REST_Response
	 */
	public function get_assignment( $request ) {
		$args = $request->get_query_params();

		if ( ! isset( $args['experiment_name'] ) ) {
			return new \WP_Error(
				'woocommerce_rest_experiment_name_required',
				__( 'Sorry, experiment_name is required.', 'woocommerce' ),
				array( 'status' => 400 )
			);
		}

		unset( $args['rest_route'] );

		$abtest   = new \WooCommerce\Admin\Experimental_Abtest(
			$request->get_param( 'anon_id' ) ?? '',
			'woocommerce',
			true, // set consent to true here since frontend has checked it already.
			true  // set true to send request as auth user.
		);
		$response = $abtest->request_assignment( $args );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		return json_decode( $response['body'], true );
	}
}
Admin/API/Notes.php000064400000063140152333011040010002 0ustar00<?php
/**
 * REST API Admin Notes controller
 *
 * Handles requests to the admin notes endpoint.
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\Notes as NotesRepository;

/**
 * REST API Admin Notes controller class.
 *
 * @internal
 * @extends WC_REST_CRUD_Controller
 */
class Notes extends \WC_REST_CRUD_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'admin/notes';

	/**
	 * Allowed promo notes for experimental-activate-promo.
	 *
	 * @var array
	 */
	protected $allowed_promo_notes = array(
		'wcpay-promo-2022-us-incentive-20-off',
	);

	/**
	 * Register the routes for admin notes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d-]+)',
			array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique ID for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
				),
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/delete/(?P<id>[\d-]+)',
			array(
				array(
					'methods'             => \WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/delete/all',
			array(
				array(
					'methods'             => \WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_all_items' ),
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
					'args'                => array(
						'status' => array(
							'description'       => __( 'Status of note.', 'woocommerce' ),
							'type'              => 'array',
							'sanitize_callback' => 'wp_parse_slug_list',
							'validate_callback' => 'rest_validate_request_arg',
							'items'             => array(
								'enum' => Note::get_allowed_statuses(),
								'type' => 'string',
							),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/tracker/(?P<note_id>[\d-]+)/user/(?P<user_id>[\d-]+)',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'track_opened_email' ),
					'permission_callback' => '__return_true',
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/update',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'batch_update_items' ),
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/experimental-activate-promo/(?P<promo_note_name>[\w-]+)',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'activate_promo_note' ),
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get a single note.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_item( $request ) {
		$note = NotesRepository::get_note( $request->get_param( 'id' ) );

		if ( ! $note ) {
			return new \WP_Error(
				'woocommerce_note_invalid_id',
				__( 'Sorry, there is no resource with that ID.', 'woocommerce' ),
				array( 'status' => 404 )
			);
		}

		if ( is_wp_error( $note ) ) {
			return $note;
		}

		$data = $this->prepare_note_data_for_response( $note, $request );

		return rest_ensure_response( $data );
	}

	/**
	 * Get all notes.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response
	 */
	public function get_items( $request ) {
		$query_args = $this->prepare_objects_query( $request );

		$notes = NotesRepository::get_notes( 'edit', $query_args );

		$data = array();
		foreach ( (array) $notes as $note_obj ) {
			$note   = $this->prepare_item_for_response( $note_obj, $request );
			$note   = $this->prepare_response_for_collection( $note );
			$data[] = $note;
		}

		$response = rest_ensure_response( $data );
		$response->header( 'X-WP-Total', count( $data ) );

		return $response;
	}

	/**
	 * Checks if user is in tasklist experiment.
	 *
	 * @return bool Whether remote inbox notifications are enabled.
	 */
	private function is_tasklist_experiment_assigned_treatment() {
		$anon_id        = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : '';
		$allow_tracking = 'yes' === get_option( 'woocommerce_allow_tracking' );
		$abtest         = new \WooCommerce\Admin\Experimental_Abtest(
			$anon_id,
			'woocommerce',
			$allow_tracking
		);

		$date = new \DateTime();
		$date->setTimeZone( new \DateTimeZone( 'UTC' ) );

		$experiment_name = sprintf(
			'woocommerce_tasklist_progression_headercard_%s_%s',
			$date->format( 'Y' ),
			$date->format( 'm' )
		);

		$experiment_name_2col = sprintf(
			'woocommerce_tasklist_progression_headercard_2col_%s_%s',
			$date->format( 'Y' ),
			$date->format( 'm' )
		);

		return $abtest->get_variation( $experiment_name ) === 'treatment' ||
			$abtest->get_variation( $experiment_name_2col ) === 'treatment';
	}

	/**
	 * Prepare objects query.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args               = array();
		$args['order']      = $request['order'];
		$args['orderby']    = $request['orderby'];
		$args['per_page']   = $request['per_page'];
		$args['page']       = $request['page'];
		$args['type']       = isset( $request['type'] ) ? $request['type'] : array();
		$args['status']     = isset( $request['status'] ) ? $request['status'] : array();
		$args['source']     = isset( $request['source'] ) ? $request['source'] : array();
		$args['is_deleted'] = 0;

		if ( 'date' === $args['orderby'] ) {
			$args['orderby'] = 'date_created';
		}

		/**
		 * Filter the query arguments for a request.
		 *
		 * Enables adding extra arguments or setting defaults for a post
		 * collection request.
		 *
		 * @param array           $args    Key value array of query var to query value.
		 * @param WP_REST_Request $request The request used.
		 * @since 3.9.0
		 */
		$args = apply_filters( 'woocommerce_rest_notes_object_query', $args, $request );

		return $args;
	}

	/**
	 * Check whether a given request has permission to read a single note.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check whether a given request has permission to read notes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Update a single note.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function update_item( $request ) {
		$note = NotesRepository::get_note( $request->get_param( 'id' ) );

		if ( ! $note ) {
			return new \WP_Error(
				'woocommerce_note_invalid_id',
				__( 'Sorry, there is no resource with that ID.', 'woocommerce' ),
				array( 'status' => 404 )
			);
		}

		NotesRepository::update_note( $note, $this->get_requested_updates( $request ) );
		return $this->get_item( $request );
	}

	/**
	 * Delete a single note.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function delete_item( $request ) {
		$note = NotesRepository::get_note( $request->get_param( 'id' ) );

		if ( ! $note ) {
			return new \WP_Error(
				'woocommerce_note_invalid_id',
				__( 'Sorry, there is no note with that ID.', 'woocommerce' ),
				array( 'status' => 404 )
			);
		}

		NotesRepository::delete_note( $note );
		$data = $this->prepare_note_data_for_response( $note, $request );
		return rest_ensure_response( $data );
	}

	/**
	 * Delete all notes.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Request|WP_Error
	 */
	public function delete_all_items( $request ) {
		$args = array();
		if ( isset( $request['status'] ) ) {
			$args['status'] = $request['status'];
		}
		$notes = NotesRepository::delete_all_notes( $args );
		$data  = array();
		foreach ( (array) $notes as $note_obj ) {
			$data[] = $this->prepare_note_data_for_response( $note_obj, $request );
		}

		$response = rest_ensure_response( $data );
		$response->header( 'X-WP-Total', NotesRepository::get_notes_count( array( 'info', 'warning' ), array() ) );
		return $response;
	}

	/**
	 * Prepare note data.
	 *
	 * @param Note            $note     Note data.
	 * @param WP_REST_Request $request  Request object.
	 *
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_note_data_for_response( $note, $request ) {
		$note = $note->get_data();
		$note = $this->prepare_item_for_response( $note, $request );
		return $this->prepare_response_for_collection( $note );
	}

	/**
	 * Prepare an array with the the requested updates.
	 *
	 * @param WP_REST_Request $request  Request object.
	 * @return array A list of the requested updates values.
	 */
	protected function get_requested_updates( $request ) {
		$requested_updates = array();
		if ( ! is_null( $request->get_param( 'status' ) ) ) {
			$requested_updates['status'] = $request->get_param( 'status' );
		}

		if ( ! is_null( $request->get_param( 'date_reminder' ) ) ) {
			$requested_updates['date_reminder'] = $request->get_param( 'date_reminder' );
		}

		if ( ! is_null( $request->get_param( 'is_deleted' ) ) ) {
			$requested_updates['is_deleted'] = $request->get_param( 'is_deleted' );
		}

		if ( ! is_null( $request->get_param( 'is_read' ) ) ) {
			$requested_updates['is_read'] = $request->get_param( 'is_read' );
		}

		return $requested_updates;
	}

	/**
	 * Batch update a set of notes.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Request|WP_Error
	 */
	public function batch_update_items( $request ) {
		$data     = array();
		$note_ids = $request->get_param( 'noteIds' );

		if ( ! isset( $note_ids ) || ! is_array( $note_ids ) ) {
			return new \WP_Error(
				'woocommerce_note_invalid_ids',
				__( 'Please provide an array of IDs through the noteIds param.', 'woocommerce' ),
				array( 'status' => 422 )
			);
		}

		foreach ( (array) $note_ids as $note_id ) {
			$note = NotesRepository::get_note( (int) $note_id );
			if ( $note ) {
				NotesRepository::update_note( $note, $this->get_requested_updates( $request ) );
				$data[] = $this->prepare_note_data_for_response( $note, $request );
			}
		}

		$response = rest_ensure_response( $data );
		$response->header( 'X-WP-Total', NotesRepository::get_notes_count( array( 'info', 'warning' ), array() ) );
		return $response;
	}

	/**
	 * Activate a promo note, create if not exist.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Request|WP_Error
	 */
	public function activate_promo_note( $request ) {
		$promo_note_name = $request->get_param( 'promo_note_name' );

		if ( ! in_array( $promo_note_name, $this->allowed_promo_notes, true ) ) {
			return new \WP_Error(
				'woocommerce_note_invalid_promo_note_name',
				__( 'Please provide a valid promo note name.', 'woocommerce' ),
				array( 'status' => 422 )
			);
		}

		$data_store = NotesRepository::load_data_store();
		$note_ids   = $data_store->get_notes_with_name( $promo_note_name );

		if ( empty( $note_ids ) ) {
			// Promo note doesn't exist, this could happen in cases where
			// user might have disabled RemoteInboxNotications via disabling
			// marketing suggestions. Thus we'd have to manually add the note.
			$note = new Note();
			$note->set_name( $promo_note_name );
			$note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED );
			$data_store->create( $note );
		} else {
			$note = NotesRepository::get_note( $note_ids[0] );
			NotesRepository::update_note(
				$note,
				[
					'status' => Note::E_WC_ADMIN_NOTE_ACTIONED,
				]
			);
		}

		return rest_ensure_response(
			array(
				'success' => true,
			)
		);
	}

	/**
	 * Makes sure the current user has access to WRITE the settings APIs.
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|bool
	 */
	public function update_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Prepare a path or query for serialization to the client.
	 *
	 * @param string $query The query, path, or URL to transform.
	 * @return string A fully formed URL.
	 */
	public function prepare_query_for_response( $query ) {
		if ( empty( $query ) ) {
			return $query;
		}
		if ( 'https://' === substr( $query, 0, 8 ) ) {
			return $query;
		}
		if ( 'http://' === substr( $query, 0, 7 ) ) {
			return $query;
		}
		if ( '?' === substr( $query, 0, 1 ) ) {
			return admin_url( 'admin.php' . $query );
		}

		return admin_url( $query );
	}

	/**
	 * Maybe add a nonce to a URL.
	 *
	 * @link https://codex.wordpress.org/WordPress_Nonces
	 *
	 * @param string $url The URL needing a nonce.
	 * @param string $action The nonce action.
	 * @param string $name The nonce anme.
	 * @return string A fully formed URL.
	 */
	private function maybe_add_nonce_to_url( string $url, string $action = '', string $name = '' ) : string {
		if ( empty( $action ) ) {
			return $url;
		}

		if ( empty( $name ) ) {
			// Default paramater name.
			$name = '_wpnonce';
		}

		return add_query_arg( $name, wp_create_nonce( $action ), $url );
	}

	/**
	 * Prepare a note object for serialization.
	 *
	 * @param array           $data Note data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $data, $request ) {
		$context                   = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data                      = $this->add_additional_fields_to_object( $data, $request );
		$data['date_created_gmt']  = wc_rest_prepare_date_response( $data['date_created'] );
		$data['date_created']      = wc_rest_prepare_date_response( $data['date_created'], false );
		$data['date_reminder_gmt'] = wc_rest_prepare_date_response( $data['date_reminder'] );
		$data['date_reminder']     = wc_rest_prepare_date_response( $data['date_reminder'], false );
		$data['title']             = stripslashes( $data['title'] );
		$data['content']           = stripslashes( $data['content'] );
		$data['is_snoozable']      = (bool) $data['is_snoozable'];
		$data['is_deleted']        = (bool) $data['is_deleted'];
		$data['is_read']           = (bool) $data['is_read'];
		foreach ( (array) $data['actions'] as $key => $value ) {
			$data['actions'][ $key ]->label  = stripslashes( $data['actions'][ $key ]->label );
			$data['actions'][ $key ]->url    = $this->maybe_add_nonce_to_url(
				$this->prepare_query_for_response( $data['actions'][ $key ]->query ),
				(string) $data['actions'][ $key ]->nonce_action,
				(string) $data['actions'][ $key ]->nonce_name
			);
			$data['actions'][ $key ]->status = stripslashes( $data['actions'][ $key ]->status );
		}
		$data = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links(
			array(
				'self'       => array(
					'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $data['id'] ) ),
				),
				'collection' => array(
					'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
				),
			)
		);
		/**
		 * Filter a note returned from the API.
		 *
		 * Allows modification of the note data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param array            $data The original note.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 * @since 3.9.0
		 */
		return apply_filters( 'woocommerce_rest_prepare_note', $response, $data, $request );
	}


	/**
	 * Track opened emails.
	 *
	 * @param WP_REST_Request $request Request object.
	 */
	public function track_opened_email( $request ) {
		$note = NotesRepository::get_note( $request->get_param( 'note_id' ) );
		if ( ! $note ) {
			return;
		}

		NotesRepository::record_tracks_event_with_user( $request->get_param( 'user_id' ), 'email_note_opened', array( 'note_name' => $note->get_name() ) );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params             = array();
		$params['context']  = $this->get_context_param( array( 'default' => 'view' ) );
		$params['order']    = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']  = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'note_id',
				'date',
				'type',
				'title',
				'status',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['page']     = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page'] = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['type']     = array(
			'description'       => __( 'Type of note.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'enum' => Note::get_allowed_types(),
				'type' => 'string',
			),
		);
		$params['status']   = array(
			'description'       => __( 'Status of note.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'enum' => Note::get_allowed_statuses(),
				'type' => 'string',
			),
		);
		$params['source']   = array(
			'description'       => __( 'Source of note.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);
		return $params;
	}

	/**
	 * Get the note's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'note',
			'type'       => 'object',
			'properties' => array(
				'id'                => array(
					'description' => __( 'ID of the note record.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'name'              => array(
					'description' => __( 'Name of the note.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'type'              => array(
					'description' => __( 'The type of the note (e.g. error, warning, etc.).', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'locale'            => array(
					'description' => __( 'Locale used for the note title and content.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'title'             => array(
					'description' => __( 'Title of the note.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'content'           => array(
					'description' => __( 'Content of the note.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'content_data'      => array(
					'description' => __( 'Content data for the note. JSON string. Available for re-localization.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'status'            => array(
					'description' => __( 'The status of the note (e.g. unactioned, actioned).', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'source'            => array(
					'description' => __( 'Source of the note.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'      => array(
					'description' => __( 'Date the note was created.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt'  => array(
					'description' => __( 'Date the note was created (GMT).', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_reminder'     => array(
					'description' => __( 'Date after which the user should be reminded of the note, if any.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true, // @todo Allow date_reminder to be updated.
				),
				'date_reminder_gmt' => array(
					'description' => __( 'Date after which the user should be reminded of the note, if any (GMT).', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'is_snoozable'      => array(
					'description' => __( 'Whether or not a user can request to be reminded about the note.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'actions'           => array(
					'description' => __( 'An array of actions, if any, for the note.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'layout'            => array(
					'description' => __( 'The layout of the note (e.g. banner, thumbnail, plain).', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'image'             => array(
					'description' => __( 'The image of the note, if any.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'is_deleted'        => array(
					'description' => __( 'Registers whether the note is deleted or not', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'is_read'           => array(
					'description' => __( 'Registers whether the note is read or not', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);
		return $this->add_additional_fields_schema( $schema );
	}
}
Admin/API/Features.php000064400000003314152333011040010465 0ustar00<?php
/**
 * REST API Features Controller
 *
 * Handles requests to /features
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Features\Features as FeaturesClass;

/**
 * Features Controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class Features extends \WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'features';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_features' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to read onboarding profile data.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Return available payment methods.
	 *
	 * @param \WP_REST_Request $request Request data.
	 *
	 * @return \WP_Error|\WP_REST_Response
	 */
	public function get_features( $request ) {
		return FeaturesClass::get_available_features();
	}

}
Admin/API/NoteActions.php000064400000004622152333011040011140 0ustar00<?php
/**
 * REST API Admin Note Action controller
 *
 * Handles requests to the admin note action endpoint.
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\Notes as NotesFactory;

/**
 * REST API Admin Note Action controller class.
 *
 * @internal
 * @extends WC_REST_CRUD_Controller
 */
class NoteActions extends Notes {

	/**
	 * Register the routes for admin notes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<note_id>[\d-]+)/action/(?P<action_id>[\d-]+)',
			array(
				'args'   => array(
					'note_id'   => array(
						'description' => __( 'Unique ID for the Note.', 'woocommerce' ),
						'type'        => 'integer',
					),
					'action_id' => array(
						'description' => __( 'Unique ID for the Note Action.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'trigger_note_action' ),
					// @todo - double check these permissions for taking note actions.
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Trigger a note action.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function trigger_note_action( $request ) {
		$note = NotesFactory::get_note( $request->get_param( 'note_id' ) );

		if ( ! $note ) {
			return new \WP_Error(
				'woocommerce_note_invalid_id',
				__( 'Sorry, there is no resource with that ID.', 'woocommerce' ),
				array( 'status' => 404 )
			);
		}

		$note->set_is_read( true );
		$note->save();

		$triggered_action = NotesFactory::get_action_by_id( $note, $request->get_param( 'action_id' ) );

		if ( ! $triggered_action ) {
			return new \WP_Error(
				'woocommerce_note_action_invalid_id',
				__( 'Sorry, there is no resource with that ID.', 'woocommerce' ),
				array( 'status' => 404 )
			);
		}

		$triggered_note = NotesFactory::trigger_note_action( $note, $triggered_action );

		$data = $triggered_note->get_data();
		$data = $this->prepare_item_for_response( $data, $request );
		$data = $this->prepare_response_for_collection( $data );

		return rest_ensure_response( $data );
	}
}
Admin/API/Plugins.php000064400000047603152333011040010341 0ustar00<?php
/**
 * REST API Plugins Controller
 *
 * Handles requests to install and activate depedent plugins.
 */

namespace Automattic\WooCommerce\Admin\API;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\PaymentMethodSuggestionsDataSourcePoller;
use Automattic\WooCommerce\Admin\PluginsHelper;
use \Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins;

defined( 'ABSPATH' ) || exit;

/**
 * Plugins Controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class Plugins extends \WC_REST_Data_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'plugins';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/install',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'install_plugins' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/install/status',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_installation_status' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/install/status/(?P<job_id>[a-z0-9_\-]+)',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_job_installation_status' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/active',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'active_plugins' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/installed',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'installed_plugins' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/activate',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'activate_plugins' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/activate/status',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_activation_status' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/activate/status/(?P<job_id>[a-z0-9_\-]+)',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_job_activation_status' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/connect-jetpack',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'connect_jetpack' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_connect_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/request-wccom-connect',
			array(
				array(
					'methods'             => 'POST',
					'callback'            => array( $this, 'request_wccom_connect' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_connect_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/finish-wccom-connect',
			array(
				array(
					'methods'             => 'POST',
					'callback'            => array( $this, 'finish_wccom_connect' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_connect_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/connect-wcpay',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'connect_wcpay' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_connect_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/connect-square',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'connect_square' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_connect_schema' ),
			)
		);
	}

	/**
	 * Check if a given request has access to manage plugins.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_item_permissions_check( $request ) {
		if ( ! current_user_can( 'install_plugins' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage plugins.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Install the requested plugin.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|array Plugin Status
	 */
	public function install_plugin( $request ) {
		wc_deprecated_function( 'install_plugin', '4.3', '\Automattic\WooCommerce\Admin\API\Plugins()->install_plugins' );
		// This method expects a `plugin` argument to be sent, install plugins requires plugins.
		$request['plugins'] = $request['plugin'];
		return self::install_plugins( $request );
	}

	/**
	 * Installs the requested plugins.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|array Plugin Status
	 */
	public function install_plugins( $request ) {
		$plugins = explode( ',', $request['plugins'] );

		if ( empty( $request['plugins'] ) || ! is_array( $plugins ) ) {
			return new \WP_Error( 'woocommerce_rest_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce' ), 404 );
		}

		if ( isset( $request['async'] ) && $request['async'] ) {
			$job_id = PluginsHelper::schedule_install_plugins( $plugins );

			return array(
				'data'    => array(
					'job_id'  => $job_id,
					'plugins' => $plugins,
				),
				'message' => __( 'Plugin installation has been scheduled.', 'woocommerce' ),
			);
		}

		$data = PluginsHelper::install_plugins( $plugins );

		return array(
			'data'    => array(
				'installed'    => $data['installed'],
				'results'      => $data['results'],
				'install_time' => $data['time'],
			),
			'errors'  => $data['errors'],
			'success' => count( $data['errors']->errors ) === 0,
			'message' => count( $data['errors']->errors ) === 0
				? __( 'Plugins were successfully installed.', 'woocommerce' )
				: __( 'There was a problem installing some of the requested plugins.', 'woocommerce' ),
		);
	}

	/**
	 * Returns a list of recently scheduled installation jobs.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array Jobs.
	 */
	public function get_installation_status( $request ) {
		return PluginsHelper::get_installation_status();
	}

	/**
	 * Returns a list of recently scheduled installation jobs.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array Job.
	 */
	public function get_job_installation_status( $request ) {
		$job_id = $request->get_param( 'job_id' );
		$jobs   = PluginsHelper::get_installation_status( $job_id );
		return reset( $jobs );
	}

	/**
	 * Returns a list of active plugins in API format.
	 *
	 * @return array Active plugins
	 */
	public static function active_plugins() {
		return( array(
			'plugins' => array_values( PluginsHelper::get_active_plugin_slugs() ),
		) );
	}
	/**
	 * Returns a list of active plugins.
	 *
	 * @internal
	 * @return array Active plugins
	 */
	public static function get_active_plugins() {
		$data = self::active_plugins();
		return $data['plugins'];
	}

	/**
	 * Returns a list of installed plugins.
	 *
	 * @return array Installed plugins
	 */
	public function installed_plugins() {
		return( array(
			'plugins' => PluginsHelper::get_installed_plugin_slugs(),
		) );
	}

	/**
	 * Activate the requested plugin.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|array Plugin Status
	 */
	public function activate_plugins( $request ) {
		$plugins = explode( ',', $request['plugins'] );

		if ( empty( $request['plugins'] ) || ! is_array( $plugins ) ) {
			return new \WP_Error( 'woocommerce_rest_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce' ), 404 );
		}

		if ( isset( $request['async'] ) && $request['async'] ) {
			$job_id = PluginsHelper::schedule_activate_plugins( $plugins );

			return array(
				'data'    => array(
					'job_id'  => $job_id,
					'plugins' => $plugins,
				),
				'message' => __( 'Plugin activation has been scheduled.', 'woocommerce' ),
			);
		}

		$data = PluginsHelper::activate_plugins( $plugins );

		return( array(
			'data'    => array(
				'activated' => $data['activated'],
				'active'    => $data['active'],
			),
			'errors'  => $data['errors'],
			'success' => count( $data['errors']->errors ) === 0,
			'message' => count( $data['errors']->errors ) === 0
				? __( 'Plugins were successfully activated.', 'woocommerce' )
				: __( 'There was a problem activating some of the requested plugins.', 'woocommerce' ),
		) );
	}

	/**
	 * Returns a list of recently scheduled activation jobs.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array Job.
	 */
	public function get_activation_status( $request ) {
		return PluginsHelper::get_activation_status();
	}

	/**
	 * Returns a list of recently scheduled activation jobs.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array Jobs.
	 */
	public function get_job_activation_status( $request ) {
		$job_id = $request->get_param( 'job_id' );
		$jobs   = PluginsHelper::get_activation_status( $job_id );
		return reset( $jobs );
	}

	/**
	 * Generates a Jetpack Connect URL.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|array Connection URL for Jetpack
	 */
	public function connect_jetpack( $request ) {
		if ( ! class_exists( '\Jetpack' ) ) {
			return new \WP_Error( 'woocommerce_rest_jetpack_not_active', __( 'Jetpack is not installed or active.', 'woocommerce' ), 404 );
		}

		$redirect_url = apply_filters( 'woocommerce_admin_onboarding_jetpack_connect_redirect_url', esc_url_raw( $request['redirect_url'] ) );
		$connect_url  = \Jetpack::init()->build_connect_url( true, $redirect_url, 'woocommerce-onboarding' );

		$calypso_env = defined( 'WOOCOMMERCE_CALYPSO_ENVIRONMENT' ) && in_array( WOOCOMMERCE_CALYPSO_ENVIRONMENT, array( 'development', 'wpcalypso', 'horizon', 'stage' ), true ) ? WOOCOMMERCE_CALYPSO_ENVIRONMENT : 'production';
		$connect_url = add_query_arg( array( 'calypso_env' => $calypso_env ), $connect_url );

		return( array(
			'slug'          => 'jetpack',
			'name'          => __( 'Jetpack', 'woocommerce' ),
			'connectAction' => $connect_url,
		) );
	}

	/**
	 *  Kicks off the WCCOM Connect process.
	 *
	 * @return WP_Error|array Connection URL for WooCommerce.com
	 */
	public function request_wccom_connect() {
		include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-api.php';
		if ( ! class_exists( 'WC_Helper_API' ) ) {
			return new \WP_Error( 'woocommerce_rest_helper_not_active', __( 'There was an error loading the WooCommerce.com Helper API.', 'woocommerce' ), 404 );
		}

		$redirect_uri = wc_admin_url( '&task=connect&wccom-connected=1' );

		$request = \WC_Helper_API::post(
			'oauth/request_token',
			array(
				'body' => array(
					'home_url'     => home_url(),
					'redirect_uri' => $redirect_uri,
				),
			)
		);

		$code = wp_remote_retrieve_response_code( $request );
		if ( 200 !== $code ) {
			return new \WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to WooCommerce.com. Please try again.', 'woocommerce' ), 500 );
		}

		$secret = json_decode( wp_remote_retrieve_body( $request ) );
		if ( empty( $secret ) ) {
			return new \WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to WooCommerce.com. Please try again.', 'woocommerce' ), 500 );
		}

		do_action( 'woocommerce_helper_connect_start' );

		$connect_url = add_query_arg(
			array(
				'home_url'     => rawurlencode( home_url() ),
				'redirect_uri' => rawurlencode( $redirect_uri ),
				'secret'       => rawurlencode( $secret ),
				'wccom-from'   => 'onboarding',
			),
			\WC_Helper_API::url( 'oauth/authorize' )
		);

		if ( defined( 'WOOCOMMERCE_CALYPSO_ENVIRONMENT' ) && in_array( WOOCOMMERCE_CALYPSO_ENVIRONMENT, array( 'development', 'wpcalypso', 'horizon', 'stage' ), true ) ) {
			$connect_url = add_query_arg(
				array(
					'calypso_env' => WOOCOMMERCE_CALYPSO_ENVIRONMENT,
				),
				$connect_url
			);
		}

		return( array(
			'connectAction' => $connect_url,
		) );
	}

	/**
	 * Finishes connecting to WooCommerce.com.
	 *
	 * @param  object $rest_request Request details.
	 * @return WP_Error|array Contains success status.
	 */
	public function finish_wccom_connect( $rest_request ) {
		include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper.php';
		include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-api.php';
		include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-updater.php';
		include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-options.php';
		if ( ! class_exists( 'WC_Helper_API' ) ) {
			return new \WP_Error( 'woocommerce_rest_helper_not_active', __( 'There was an error loading the WooCommerce.com Helper API.', 'woocommerce' ), 404 );
		}

		// Obtain an access token.
		$request = \WC_Helper_API::post(
			'oauth/access_token',
			array(
				'body' => array(
					'request_token' => wp_unslash( $rest_request['request_token'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					'home_url'      => home_url(),
				),
			)
		);

		$code = wp_remote_retrieve_response_code( $request );
		if ( 200 !== $code ) {
			return new \WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to WooCommerce.com. Please try again.', 'woocommerce' ), 500 );
		}

		$access_token = json_decode( wp_remote_retrieve_body( $request ), true );
		if ( ! $access_token ) {
			return new \WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to WooCommerce.com. Please try again.', 'woocommerce' ), 500 );
		}

		\WC_Helper_Options::update(
			'auth',
			array(
				'access_token'        => $access_token['access_token'],
				'access_token_secret' => $access_token['access_token_secret'],
				'site_id'             => $access_token['site_id'],
				'user_id'             => get_current_user_id(),
				'updated'             => time(),
			)
		);

		if ( ! \WC_Helper::_flush_authentication_cache() ) {
			\WC_Helper_Options::update( 'auth', array() );
			return new \WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to WooCommerce.com. Please try again.', 'woocommerce' ), 500 );
		}

		delete_transient( '_woocommerce_helper_subscriptions' );
		\WC_Helper_Updater::flush_updates_cache();

		do_action( 'woocommerce_helper_connected' );

		return array(
			'success' => true,
		);
	}


	/**
	 * Returns a URL that can be used to connect to Square.
	 *
	 * @return WP_Error|array Connect URL.
	 */
	public function connect_square() {
		if ( ! class_exists( '\WooCommerce\Square\Handlers\Connection' ) ) {
			return new \WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to Square.', 'woocommerce' ), 500 );
		}

		if ( 'US' === WC()->countries->get_base_country() ) {
			$profile = get_option( OnboardingProfile::DATA_OPTION, array() );
			if ( ! empty( $profile['industry'] ) ) {
				$has_cbd_industry = in_array( 'cbd-other-hemp-derived-products', array_column( $profile['industry'], 'slug' ), true );
			}
		}

		if ( $has_cbd_industry ) {
			$url = 'https://squareup.com/t/f_partnerships/d_referrals/p_woocommerce/c_general/o_none/l_us/dt_alldevice/pr_payments/?route=/solutions/cbd';
		} else {
			$url = \WooCommerce\Square\Handlers\Connection::CONNECT_URL_PRODUCTION;
		}

		$redirect_url = wp_nonce_url( wc_admin_url( '&task=payments&method=square&square-connect-finish=1' ), 'wc_square_connected' );
		$args         = array(
			'redirect' => rawurlencode( rawurlencode( $redirect_url ) ),
			'scopes'   => implode(
				',',
				array(
					'MERCHANT_PROFILE_READ',
					'PAYMENTS_READ',
					'PAYMENTS_WRITE',
					'ORDERS_READ',
					'ORDERS_WRITE',
					'CUSTOMERS_READ',
					'CUSTOMERS_WRITE',
					'SETTLEMENTS_READ',
					'ITEMS_READ',
					'ITEMS_WRITE',
					'INVENTORY_READ',
					'INVENTORY_WRITE',
				)
			),
		);

		$connect_url = add_query_arg( $args, $url );

		return( array(
			'connectUrl' => $connect_url,
		) );
	}

	/**
	 * Returns a URL that can be used to by WCPay to verify business details with Stripe.
	 *
	 * @return WP_Error|array Connect URL.
	 */
	public function connect_wcpay() {
		if ( ! class_exists( 'WC_Payments_Account' ) ) {
			return new \WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error communicating with the WooCommerce Payments plugin.', 'woocommerce' ), 500 );
		}

		$connect_url = add_query_arg(
			array(
				'wcpay-connect' => 'WCADMIN_PAYMENT_TASK',
				'_wpnonce'      => wp_create_nonce( 'wcpay-connect' ),
			),
			admin_url()
		);

		return( array(
			'connectUrl' => $connect_url,
		) );
	}

	/**
	 * Get the schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'plugins',
			'type'       => 'object',
			'properties' => array(
				'slug'   => array(
					'description' => __( 'Plugin slug.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'   => array(
					'description' => __( 'Plugin name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'status' => array(
					'description' => __( 'Plugin status.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_connect_schema() {
		$schema = $this->get_item_schema();
		unset( $schema['properties']['status'] );
		$schema['properties']['connectAction'] = array(
			'description' => __( 'Action that should be completed to connect Jetpack.', 'woocommerce' ),
			'type'        => 'string',
			'context'     => array( 'view', 'edit' ),
			'readonly'    => true,
		);
		return $schema;
	}
}
Admin/API/Init.php000064400000017060152333011040007615 0ustar00<?php
/**
 * REST API bootstrap.
 */

namespace Automattic\WooCommerce\Admin\API;

use Automattic\WooCommerce\Admin\Features\Features;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Internal\Admin\Loader;

/**
 * Init class.
 *
 * @internal
 */
class Init {
	/**
	 * The single instance of the class.
	 *
	 * @var object
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 *
	 * @return object Instance.
	 */
	final public static function instance() {
		if ( null === static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Boostrap REST API.
	 */
	public function __construct() {
		// Hook in data stores.
		add_filter( 'woocommerce_data_stores', array( __CLASS__, 'add_data_stores' ) );
		// REST API extensions init.
		add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );

		// Add currency symbol to orders endpoint response.
		add_filter( 'woocommerce_rest_prepare_shop_order_object', array( __CLASS__, 'add_currency_symbol_to_order_response' ) );
	}

	/**
	 * Init REST API.
	 */
	public function rest_api_init() {
		$controllers = array(
			'Automattic\WooCommerce\Admin\API\Features',
			'Automattic\WooCommerce\Admin\API\Notes',
			'Automattic\WooCommerce\Admin\API\NoteActions',
			'Automattic\WooCommerce\Admin\API\Coupons',
			'Automattic\WooCommerce\Admin\API\Data',
			'Automattic\WooCommerce\Admin\API\DataCountries',
			'Automattic\WooCommerce\Admin\API\DataDownloadIPs',
			'Automattic\WooCommerce\Admin\API\Experiments',
			'Automattic\WooCommerce\Admin\API\Marketing',
			'Automattic\WooCommerce\Admin\API\MarketingOverview',
			'Automattic\WooCommerce\Admin\API\Options',
			'Automattic\WooCommerce\Admin\API\Orders',
			'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions',
			'Automattic\WooCommerce\Admin\API\Products',
			'Automattic\WooCommerce\Admin\API\ProductAttributes',
			'Automattic\WooCommerce\Admin\API\ProductAttributeTerms',
			'Automattic\WooCommerce\Admin\API\ProductCategories',
			'Automattic\WooCommerce\Admin\API\ProductVariations',
			'Automattic\WooCommerce\Admin\API\ProductReviews',
			'Automattic\WooCommerce\Admin\API\ProductVariations',
			'Automattic\WooCommerce\Admin\API\ProductsLowInStock',
			'Automattic\WooCommerce\Admin\API\SettingOptions',
			'Automattic\WooCommerce\Admin\API\Themes',
			'Automattic\WooCommerce\Admin\API\Plugins',
			'Automattic\WooCommerce\Admin\API\OnboardingFreeExtensions',
			'Automattic\WooCommerce\Admin\API\OnboardingProductTypes',
			'Automattic\WooCommerce\Admin\API\OnboardingProfile',
			'Automattic\WooCommerce\Admin\API\OnboardingTasks',
			'Automattic\WooCommerce\Admin\API\OnboardingThemes',
			'Automattic\WooCommerce\Admin\API\NavigationFavorites',
			'Automattic\WooCommerce\Admin\API\Taxes',
			'Automattic\WooCommerce\Admin\API\MobileAppMagicLink',
		);

		if ( Features::is_enabled( 'analytics' ) ) {
			$analytics_controllers = array(
				'Automattic\WooCommerce\Admin\API\Customers',
				'Automattic\WooCommerce\Admin\API\Leaderboards',
				'Automattic\WooCommerce\Admin\API\Reports\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Import\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Export\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Products\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Variations\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Products\Stats\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Variations\Stats\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Revenue\Stats\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Orders\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Categories\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Taxes\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Coupons\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Stock\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Downloads\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Customers\Controller',
				'Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\Controller',
			);

			// The performance indicators controller must be registered last, after other /stats endpoints have been registered.
			$analytics_controllers[] = 'Automattic\WooCommerce\Admin\API\Reports\PerformanceIndicators\Controller';

			$controllers = array_merge( $controllers, $analytics_controllers );
		}

		$controllers = apply_filters( 'woocommerce_admin_rest_controllers', $controllers );

		foreach ( $controllers as $controller ) {
			$this->$controller = new $controller();
			$this->$controller->register_routes();
		}
	}

	/**
	 * Adds data stores.
	 *
	 * @internal
	 * @param array $data_stores List of data stores.
	 * @return array
	 */
	public static function add_data_stores( $data_stores ) {
		return array_merge(
			$data_stores,
			array(
				'report-revenue-stats'    => 'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore',
				'report-orders'           => 'Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore',
				'report-orders-stats'     => 'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore',
				'report-products'         => 'Automattic\WooCommerce\Admin\API\Reports\Products\DataStore',
				'report-variations'       => 'Automattic\WooCommerce\Admin\API\Reports\Variations\DataStore',
				'report-products-stats'   => 'Automattic\WooCommerce\Admin\API\Reports\Products\Stats\DataStore',
				'report-variations-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Variations\Stats\DataStore',
				'report-categories'       => 'Automattic\WooCommerce\Admin\API\Reports\Categories\DataStore',
				'report-taxes'            => 'Automattic\WooCommerce\Admin\API\Reports\Taxes\DataStore',
				'report-taxes-stats'      => 'Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\DataStore',
				'report-coupons'          => 'Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore',
				'report-coupons-stats'    => 'Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\DataStore',
				'report-downloads'        => 'Automattic\WooCommerce\Admin\API\Reports\Downloads\DataStore',
				'report-downloads-stats'  => 'Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats\DataStore',
				'admin-note'              => 'Automattic\WooCommerce\Admin\Notes\DataStore',
				'report-customers'        => 'Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore',
				'report-customers-stats'  => 'Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\DataStore',
				'report-stock-stats'      => 'Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\DataStore',
			)
		);
	}

	/**
	 * Add the currency symbol (in addition to currency code) to each Order
	 * object in REST API responses. For use in formatAmount().
	 *
	 * @internal
	 * @param {WP_REST_Response} $response REST response object.
	 * @returns {WP_REST_Response}
	 */
	public static function add_currency_symbol_to_order_response( $response ) {
		$response_data                    = $response->get_data();
		$currency_code                    = $response_data['currency'];
		$currency_symbol                  = get_woocommerce_currency_symbol( $currency_code );
		$response_data['currency_symbol'] = html_entity_decode( $currency_symbol );
		$response->set_data( $response_data );

		return $response;
	}
}
Admin/API/Customers.php000064400000004163152333011040010676 0ustar00<?php
/**
 * REST API Customers Controller
 *
 * Handles requests to /customers/*
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Customers controller.
 *
 * @internal
 * @extends \Automattic\WooCommerce\Admin\API\Reports\Customers\Controller
 */
class Customers extends \Automattic\WooCommerce\Admin\API\Reports\Customers\Controller {

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'customers';

	/**
	 * Register the routes for customers.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d-]+)',
			array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique ID for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args              = parent::prepare_reports_query( $request );
		$args['customers'] = $request['include'];
		return $args;
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params            = parent::get_collection_params();
		$params['include'] = $params['customers'];
		unset( $params['customers'] );
		return $params;
	}
}
Admin/API/ProductAttributes.php000064400000010730152333011040012376 0ustar00<?php
/**
 * REST API Product Attributes Controller
 *
 * Handles requests to /products/attributes.
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Product categories controller.
 *
 * @internal
 * @extends WC_REST_Product_Attributes_Controller
 */
class ProductAttributes extends \WC_REST_Product_Attributes_Controller {
	use CustomAttributeTraits;

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Register the routes for custom product attributes.
	 */
	public function register_routes() {
		parent::register_routes();

		register_rest_route(
			$this->namespace,
			'products/attributes/(?P<slug>[a-z0-9_\-]+)',
			array(
				'args'   => array(
					'slug' => array(
						'description' => __( 'Slug identifier for the resource.', 'woocommerce' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item_by_slug' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get the query params for collections
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params           = parent::get_collection_params();
		$params['search'] = array(
			'description'       => __( 'Search by similar attribute name.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get the Attribute's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = parent::get_item_schema();
		// Custom attributes substitute slugs for numeric IDs.
		$schema['properties']['id']['type'] = array( 'integer', 'string' );

		return $schema;
	}

	/**
	 * Get a single attribute by it's slug.
	 *
	 * @param WP_REST_Request $request The API request.
	 * @return WP_REST_Response
	 */
	public function get_item_by_slug( $request ) {
		if ( empty( $request['slug'] ) ) {
			return array();
		}

		$attributes = $this->get_custom_attribute_by_slug( $request['slug'] );

		if ( is_wp_error( $attributes ) ) {
			return $attributes;
		}

		$response_items = $this->format_custom_attribute_items_for_response( $attributes );

		return reset( $response_items );
	}

	/**
	 * Format custom attribute items for response (mimic the structure of a taxonomy - backed attribute).
	 *
	 * @param array $custom_attributes - CustomAttributeTraits::get_custom_attributes().
	 * @return array
	 */
	protected function format_custom_attribute_items_for_response( $custom_attributes ) {
		$response = array();

		foreach ( $custom_attributes as $attribute_key => $attribute_value ) {
			$data = array(
				'id'           => $attribute_key,
				'name'         => $attribute_value['name'],
				'slug'         => $attribute_key,
				'type'         => 'select',
				'order_by'     => 'menu_order',
				'has_archives' => false,
			);

			$item_response = rest_ensure_response( $data );
			$item_response->add_links( $this->prepare_links( (object) array( 'attribute_id' => $attribute_key ) ) );
			$item_response = $this->prepare_response_for_collection(
				$item_response
			);

			$response[] = $item_response;
		}

		return $response;
	}

	/**
	 * Get all attributes, with support for searching (which includes custom attributes).
	 *
	 * @param WP_REST_Request $request The API request.
	 * @return WP_REST_Response
	 */
	public function get_items( $request ) {
		if ( empty( $request['search'] ) ) {
			return parent::get_items( $request );
		}

		$search_string       = $request['search'];
		$custom_attributes   = $this->get_custom_attributes( array( 'name' => $search_string ) );
		$matching_attributes = $this->format_custom_attribute_items_for_response( $custom_attributes );
		$taxonomy_attributes = wc_get_attribute_taxonomies();

		foreach ( $taxonomy_attributes as $attribute_obj ) {
			// Skip taxonomy attributes that didn't match the query.
			if ( false === stripos( $attribute_obj->attribute_label, $search_string ) ) {
				continue;
			}

			$attribute             = $this->prepare_item_for_response( $attribute_obj, $request );
			$matching_attributes[] = $this->prepare_response_for_collection( $attribute );
		}

		$response = rest_ensure_response( $matching_attributes );
		$response->header( 'X-WP-Total', count( $matching_attributes ) );
		$response->header( 'X-WP-TotalPages', 1 );

		return $response;
	}
}
Admin/API/Options.php000064400000017331152333011040010346 0ustar00<?php
/**
 * REST API Options Controller
 *
 * Handles requests to get and update options in the wp_options table.
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Options Controller.
 *
 * @deprecated since 6.2.0
 *
 * @extends WC_REST_Data_Controller
 */
class Options extends \WC_REST_Data_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'options';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_options' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_options' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);
	}

	/**
	 * Check if a given request has access to get options.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		$params = explode( ',', $request['options'] );

		if ( ! isset( $request['options'] ) || ! is_array( $params ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'You must supply an array of options.', 'woocommerce' ), 500 );
		}

		foreach ( $params as $option ) {
			if ( ! $this->user_has_permission( $option, $request ) ) {
				return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view these options.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
			}
		}

		return true;
	}

	/**
	 * Check if the user has permission given an option name.
	 *
	 * @param  string          $option Option name.
	 * @param  WP_REST_Request $request Full details about the request.
	 * @param  bool            $is_update If the request is to update the option.
	 * @return boolean
	 */
	public function user_has_permission( $option, $request, $is_update = false ) {
		$permissions = $this->get_option_permissions( $request );

		if ( isset( $permissions[ $option ] ) ) {
			return $permissions[ $option ];
		}

		wc_deprecated_function( 'Automattic\WooCommerce\Admin\API\Options::' . ( $is_update ? 'update_options' : 'get_options' ), '3.1' );
		return current_user_can( 'manage_options' );
	}

	/**
	 * Check if a given request has access to update options.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_item_permissions_check( $request ) {
		$params = $request->get_json_params();

		if ( ! is_array( $params ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'You must supply an array of options and values.', 'woocommerce' ), 500 );
		}

		foreach ( $params as $option_name => $option_value ) {
			if ( ! $this->user_has_permission( $option_name, $request, true ) ) {
				return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage these options.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
			}
		}

		return true;
	}

	/**
	 * Get an array of options and respective permissions for the current user.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array
	 */
	public function get_option_permissions( $request ) {
		$permissions = self::get_default_option_permissions();
		return apply_filters_deprecated( 'woocommerce_rest_api_option_permissions', array( $permissions, $request ), '3.1.0' );
	}

	/**
	 * Get the default available option permissions.
	 *
	 * @return array
	 */
	public static function get_default_option_permissions() {
		$is_woocommerce_admin    = \Automattic\WooCommerce\Internal\Admin\Homescreen::is_admin_user();
		$woocommerce_permissions = array(
			'woocommerce_setup_jetpack_opted_in',
			'woocommerce_stripe_settings',
			'woocommerce-ppcp-settings',
			'woocommerce_ppcp-gateway_setting',
			'woocommerce_demo_store',
			'woocommerce_demo_store_notice',
			'woocommerce_ces_tracks_queue',
			'woocommerce_navigation_intro_modal_dismissed',
			'woocommerce_shipping_dismissed_timestamp',
			'woocommerce_allow_tracking',
			'woocommerce_task_list_keep_completed',
			'woocommerce_task_list_prompt_shown',
			'woocommerce_default_homepage_layout',
			'woocommerce_setup_jetpack_opted_in',
			'woocommerce_no_sales_tax',
			'woocommerce_calc_taxes',
			'woocommerce_bacs_settings',
			'woocommerce_bacs_accounts',
			'woocommerce_task_list_prompt_shown',
			'woocommerce_settings_shipping_recommendations_hidden',
			'woocommerce_task_list_dismissed_tasks',
			'woocommerce_setting_payments_recommendations_hidden',
			'woocommerce_navigation_favorites_tooltip_hidden',
			'woocommerce_marketing_overview_welcome_hidden',
			'woocommerce_admin_transient_notices_queue',
			'woocommerce_task_list_welcome_modal_dismissed',
			'woocommerce_welcome_from_calypso_modal_dismissed',
			'woocommerce_task_list_hidden',
			'woocommerce_task_list_complete',
			'woocommerce_extended_task_list_hidden',
			'woocommerce_ces_shown_for_actions',
			'woocommerce_clear_ces_tracks_queue_for_page',
			'woocommerce_admin_install_timestamp',
			'woocommerce_task_list_tracked_completed_tasks',
			'woocommerce_show_marketplace_suggestions',
			'woocommerce_task_list_reminder_bar_hidden',
			'wc_connect_options',
			'woocommerce_admin_created_default_shipping_zones',
			'woocommerce_admin_reviewed_default_shipping_zones',
			'woocommerce_admin_reviewed_store_location_settings',
		);

		$theme_permissions = array(
			'theme_mods_' . get_stylesheet() => current_user_can( 'edit_theme_options' ),
			'stylesheet'                     => current_user_can( 'edit_theme_options' ),
		);

		return array_merge(
			array_fill_keys( $theme_permissions, current_user_can( 'edit_theme_options' ) ),
			array_fill_keys( $woocommerce_permissions, $is_woocommerce_admin )
		);
	}

	/**
	 * Gets an array of options and respective values.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array Options object with option values.
	 */
	public function get_options( $request ) {
		$params  = explode( ',', $request['options'] );
		$options = array();

		if ( ! is_array( $params ) ) {
			return array();
		}

		foreach ( $params as $option ) {
			$options[ $option ] = get_option( $option );
		}

		return $options;
	}

	/**
	 * Updates an array of objects.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array Options object with a boolean if the option was updated.
	 */
	public function update_options( $request ) {
		$params  = $request->get_json_params();
		$updated = array();

		if ( ! is_array( $params ) ) {
			return array();
		}

		foreach ( $params as $key => $value ) {
			$updated[ $key ] = update_option( $key, $value );
		}

		return $updated;
	}

	/**
	 * Get the schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'options',
			'type'       => 'object',
			'properties' => array(
				'options' => array(
					'type'        => 'array',
					'description' => __( 'Array of options with associated values.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
Admin/API/DataDownloadIPs.php000064400000010230152333011040011657 0ustar00<?php
/**
 * REST API Data Download IP Controller
 *
 * Handles requests to /data/download-ips
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Data Download IP controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class DataDownloadIPs extends \WC_REST_Data_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'data/download-ips';

	/**
	 * Register routes.
	 *
	 * @since 3.5.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Return the download IPs matching the passed parameters.
	 *
	 * @since  3.5.0
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		global $wpdb;

		if ( isset( $request['match'] ) ) {
			$downloads = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT DISTINCT( user_ip_address ) FROM {$wpdb->prefix}wc_download_log
					WHERE user_ip_address LIKE %s
					LIMIT 10",
					$request['match'] . '%'
				)
			);
		} else {
			return new \WP_Error( 'woocommerce_rest_data_download_ips_invalid_request', __( 'Invalid request. Please pass the match parameter.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$data = array();

		if ( ! empty( $downloads ) ) {
			foreach ( $downloads as $download ) {
				$response = $this->prepare_item_for_response( $download, $request );
				$data[]   = $this->prepare_response_for_collection( $response );
			}
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Prepare the data object for response.
	 *
	 * @since  3.5.0
	 * @param object          $item Data object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data     = $this->add_additional_fields_to_object( $item, $request );
		$data     = $this->filter_response_by_context( $data, 'view' );
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item ) );

		/**
		 * Filter the list returned from the API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param array            $item     The original item.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_data_download_ip', $response, $item, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param object $item Data object.
	 * @return array Links for the given object.
	 */
	protected function prepare_links( $item ) {
		$links = array(
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);
		return $links;
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params            = array();
		$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
		$params['match']   = array(
			'description'       => __( 'A partial IP address can be passed and matching results will be returned.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		return $params;
	}


	/**
	 * Get the schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'data_download_ips',
			'type'       => 'object',
			'properties' => array(
				'user_ip_address' => array(
					'type'        => 'string',
					'description' => __( 'IP address.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
Admin/API/DataCountries.php000064400000002175152333011040011460 0ustar00<?php
/**
 * REST API Data countries controller.
 *
 * Handles requests to the /data/countries endpoint.
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * REST API Data countries controller class.
 *
 * @internal
 * @extends WC_REST_Data_Countries_Controller
 */
class DataCountries extends \WC_REST_Data_Countries_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Register routes.
	 *
	 * @since 3.5.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/locales',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_locales' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		parent::register_routes();
	}

	/**
	 * Get country fields.
	 *
	 * @return array
	 */
	public function get_locales() {
		$locales = WC()->countries->get_country_locale();
		return rest_ensure_response( $locales );
	}
}
Admin/API/ProductAttributeTerms.php000064400000010563152333011040013232 0ustar00<?php
/**
 * REST API Product Attribute Terms Controller
 *
 * Handles requests to /products/attributes/<slug>/terms
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Product attribute terms controller.
 *
 * @internal
 * @extends WC_REST_Product_Attribute_Terms_Controller
 */
class ProductAttributeTerms extends \WC_REST_Product_Attribute_Terms_Controller {
	use CustomAttributeTraits;

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';


	/**
	 * Register the routes for custom product attributes.
	 */
	public function register_routes() {
		parent::register_routes();

		register_rest_route(
			$this->namespace,
			'products/attributes/(?P<slug>[a-z0-9_\-]+)/terms',
			array(
				'args'   => array(
					'slug' => array(
						'description' => __( 'Slug identifier for the resource.', 'woocommerce' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item_by_slug' ),
					'permission_callback' => array( $this, 'get_custom_attribute_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check if a given request has access to read a custom attribute.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_custom_attribute_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) {
			return new WP_Error(
				'woocommerce_rest_cannot_view',
				__( 'Sorry, you cannot view this resource.', 'woocommerce' ),
				array(
					'status' => rest_authorization_required_code(),
				)
			);
		}

		return true;
	}

	/**
	 * Get the Attribute's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = parent::get_item_schema();
		// Custom attributes substitute slugs for numeric IDs.
		$schema['properties']['id']['type'] = array( 'integer', 'string' );

		return $schema;
	}

	/**
	 * Query custom attribute values by slug.
	 *
	 * @param string $slug Attribute slug.
	 * @return array Attribute values, formatted for response.
	 */
	protected function get_custom_attribute_values( $slug ) {
		global $wpdb;

		if ( empty( $slug ) ) {
			return array();
		}

		$attribute_values = array();

		// Get the attribute properties.
		$attribute = $this->get_custom_attribute_by_slug( $slug );

		if ( is_wp_error( $attribute ) ) {
			return $attribute;
		}

		// Find all attribute values assigned to products.
		$query_results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT meta_value, COUNT(meta_id) AS product_count
				FROM {$wpdb->postmeta}
				WHERE meta_key = %s
				AND meta_value != ''
				GROUP BY meta_value",
				'attribute_' . esc_sql( $slug )
			),
			OBJECT_K
		);

		// Ensure all defined properties are in the response.
		$defined_values = wc_get_text_attributes( $attribute[ $slug ]['value'] );

		foreach ( $defined_values as $defined_value ) {
			if ( array_key_exists( $defined_value, $query_results ) ) {
				continue;
			}

			$query_results[ $defined_value ] = (object) array(
				'meta_value'    => $defined_value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
				'product_count' => 0,
			);
		}

		foreach ( $query_results as $term_value => $term ) {
			// Mimic the structure of a taxonomy-backed attribute values for response.
			$data = array(
				'id'          => $term_value,
				'name'        => $term_value,
				'slug'        => $term_value,
				'description' => '',
				'menu_order'  => 0,
				'count'       => (int) $term->product_count,
			);

			$response = rest_ensure_response( $data );
			$response->add_links(
				array(
					'collection' => array(
						'href' => rest_url(
							$this->namespace . '/products/attributes/' . $slug . '/terms'
						),
					),
				)
			);
			$response = $this->prepare_response_for_collection( $response );

			$attribute_values[ $term_value ] = $response;
		}

		return array_values( $attribute_values );
	}

	/**
	 * Get a single custom attribute.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function get_item_by_slug( $request ) {
		return $this->get_custom_attribute_values( $request['slug'] );
	}
}
Admin/API/Templates/external_product.csv000064400000002212152333011040014227 0ustar00Type,Name,Published,"Is featured?","Visibility in catalog","Short description",Description,"Date sale price starts","Date sale price ends","Tax status","Tax class","In stock?",Stock,"Backorders allowed?","Sold individually?","Weight (lbs)","Length (in)","Width (in)","Height (in)","Allow customer reviews?","Purchase note","Sale price","Regular price",Categories,Tags,"Shipping class",Images,"Download limit","Download expiry days",Parent,"Grouped products",Upsells,Cross-sells,"External URL","Button text",Position,"Attribute 1 name","Attribute 1 value(s)","Attribute 1 visible","Attribute 1 global","Attribute 2 name","Attribute 2 value(s)","Attribute 2 visible","Attribute 2 global","Meta: _wpcom_is_markdown","Download 1 name","Download 1 URL","Download 2 name","Download 2 URL"
external,WordPress Pennant,0,0,visible,"This is an external product.","High Quality 36 x 12 inches Wordpress Pennant.",,,"taxable",,"1",,"0","0",,,,,"1",,,"111.05",Decor,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/pennant-1.jpg,,,,,,,"https://mercantile.wordpress.org/product/wordpress-pennant/","Buy on the WordPress swag store!",0,,,,,,,,,"1",,,,
Admin/API/Templates/digital_product.csv000064400000002332152333011040014025 0ustar00Type,Name,Published,"Is featured?","Visibility in catalog","Short description",Description,"Date sale price starts","Date sale price ends","Tax status","Tax class","In stock?",Stock,"Backorders allowed?","Sold individually?","Weight (lbs)","Length (in)","Width (in)","Height (in)","Allow customer reviews?","Purchase note","Sale price","Regular price",Categories,Tags,"Shipping class",Images,"Download limit","Download expiry days",Parent,"Grouped products",Upsells,Cross-sells,"External URL","Button text",Position,"Attribute 1 name","Attribute 1 value(s)","Attribute 1 visible","Attribute 1 global","Attribute 2 name","Attribute 2 value(s)","Attribute 2 visible","Attribute 2 global","Meta: _wpcom_is_markdown","Download 1 name","Download 1 URL","Download 2 name","Download 2 URL"
"simple, downloadable, virtual",Album,0,0,visible,"This is a simple, virtual product.","Album reviewed and chosen by Woo.",,,taxable,,1,,0,0,,,,,1,,,15,Music,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2022/05/album-1.jpg,1,1,,,,,,,0,,,,,,,,,1,"Single 1",https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg,"Single 2",https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/album.jpg
Admin/API/Templates/variable_product.csv000064400000002467152333011040014206 0ustar00Type,Name,Published,"Is featured?","Visibility in catalog","Short description",Description,"Date sale price starts","Date sale price ends","Tax status","Tax class","In stock?",Stock,"Backorders allowed?","Sold individually?","Weight (lbs)","Length (in)","Width (in)","Height (in)","Allow customer reviews?","Purchase note","Sale price","Regular price",Categories,Tags,"Shipping class",Images,"Download limit","Download expiry days",Parent,"Grouped products",Upsells,Cross-sells,"External URL","Button text",Position,"Attribute 1 name","Attribute 1 value(s)","Attribute 1 visible","Attribute 1 global","Attribute 2 name","Attribute 2 value(s)","Attribute 2 visible","Attribute 2 global","Meta: _wpcom_is_markdown","Download 1 name","Download 1 URL","Download 2 name","Download 2 URL"
variable,"V-Neck T-Shirt",0,1,visible,"This is a variable product.","Relaxed-fit T-shirt in soft cotton (Available in Blue Green and Red).",,,taxable,,1,,0,0,.5,24,1,2,1,,,,"Clothing > Tshirts",,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg",,,,,,,,,0,Color,"Blue, Green, Red",1,1,Size,"Large, Medium, Small",1,1,1,,,,
Admin/API/Templates/physical_product.csv000064400000002104152333011040014221 0ustar00Type,Name,Published,"Is featured?","Visibility in catalog","Short description",Description,"Date sale price starts","Date sale price ends","Tax status","Tax class","In stock?",Stock,"Backorders allowed?","Sold individually?","Weight (lbs)","Length (in)","Width (in)","Height (in)","Allow customer reviews?","Purchase note","Sale price","Regular price",Categories,Tags,"Shipping class",Images,"Download limit","Download expiry days",Parent,"Grouped products",Upsells,Cross-sells,"External URL","Button text",Position,"Attribute 1 name","Attribute 1 value(s)","Attribute 1 visible","Attribute 1 global","Attribute 2 name","Attribute 2 value(s)","Attribute 2 visible","Attribute 2 global","Meta: _wpcom_is_markdown","Download 1 name","Download 1 URL","Download 2 name","Download 2 URL"
simple,"Hoodie with Logo",0,0,visible,"This is a simple product.","Hoodie in sweatshirt fabric made from a cotton blend.",,,taxable,,1,,0,0,2,10,6,3,1,,,45,"Clothing > Hoodies",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg,,,,,,,,,0,Color,Blue,1,1,,,,,1,,,,
Admin/API/Templates/grouped_product.csv000064400000002573152333011040014064 0ustar00Type,Name,Published,"Is featured?","Visibility in catalog","Short description",Description,"Date sale price starts","Date sale price ends","Tax status","Tax class","In stock?",Stock,"Backorders allowed?","Sold individually?","Weight (lbs)","Length (in)","Width (in)","Height (in)","Allow customer reviews?","Purchase note","Sale price","Regular price",Categories,Tags,"Shipping class",Images,"Download limit","Download expiry days",Parent,"Grouped products",Upsells,Cross-sells,"External URL","Button text",Position,"Attribute 1 name","Attribute 1 value(s)","Attribute 1 visible","Attribute 1 global","Attribute 2 name","Attribute 2 value(s)","Attribute 2 visible","Attribute 2 global","Meta: _wpcom_is_markdown","Download 1 name","Download 1 URL","Download 2 name","Download 2 URL"
grouped,Logo Collection,0,0,visible,"This is a grouped product.","This collection consists of beanies, t-shirts and hoodies.",,,"taxable",,"1",,"0","0",,,,,"1",,,,Clothing,,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg",,,,"woo-hoodie-with-logo, woo-tshirt, woo-beanie",,,,,0,,,,,,,,,"1",,,,
Admin/API/ProductReviews.php000064400000002462152333011040011677 0ustar00<?php
/**
 * REST API Product Reviews Controller
 *
 * Handles requests to /products/reviews.
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Product reviews controller.
 *
 * @internal
 * @extends WC_REST_Product_Reviews_Controller
 */
class ProductReviews extends \WC_REST_Product_Reviews_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Prepare links for the request.
	 *
	 * @param WP_Comment $review Product review object.
	 * @return array Links for the given product review.
	 */
	protected function prepare_links( $review ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $review->comment_ID ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);
		if ( 0 !== (int) $review->comment_post_ID ) {
			$links['up'] = array(
				'href'       => rest_url( sprintf( '/%s/products/%d', $this->namespace, $review->comment_post_ID ) ),
				'embeddable' => true,
			);
		}
		if ( 0 !== (int) $review->user_id ) {
			$links['reviewer'] = array(
				'href'       => rest_url( 'wp/v2/users/' . $review->user_id ),
				'embeddable' => true,
			);
		}
		return $links;
	}
}
Admin/API/OnboardingFreeExtensions.php000064400000003514152333011040013655 0ustar00<?php
/**
 * REST API Onboarding Free Extensions Controller
 *
 * Handles requests to /onboarding/free-extensions
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\Init as RemoteFreeExtensions;

/**
 * Onboarding Payments Controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class OnboardingFreeExtensions extends \WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'onboarding/free-extensions';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_available_extensions' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to read onboarding profile data.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Return available payment methods.
	 *
	 * @param \WP_REST_Request $request Request data.
	 *
	 * @return \WP_Error|\WP_REST_Response
	 */
	public function get_available_extensions( $request ) {
		return RemoteFreeExtensions::get_extensions();
	}

}
Admin/API/MarketingOverview.php000064400000006563152333011040012370 0ustar00<?php
/**
 * REST API Marketing Overview Controller
 *
 * Handles requests to /marketing/overview.
 */

namespace Automattic\WooCommerce\Admin\API;

use Automattic\WooCommerce\Admin\Marketing\InstalledExtensions;
use Automattic\WooCommerce\Admin\PluginsHelper;

defined( 'ABSPATH' ) || exit;

/**
 * Marketing Overview Controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class MarketingOverview extends \WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'marketing/overview';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/activate-plugin',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'activate_plugin' ),
					'permission_callback' => array( $this, 'install_plugins_permissions_check' ),
					'args'                => array(
						'plugin' => array(
							'required'          => true,
							'type'              => 'string',
							'validate_callback' => 'rest_validate_request_arg',
							'sanitize_callback' => 'sanitize_title_with_dashes',
						),
					),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/installed-plugins',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_installed_plugins' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Return installed marketing extensions data.
	 *
	 * @param \WP_REST_Request $request Request data.
	 *
	 * @return \WP_Error|\WP_REST_Response
	 */
	public function activate_plugin( $request ) {
		$plugin_slug = $request->get_param( 'plugin' );

		if ( ! PluginsHelper::is_plugin_installed( $plugin_slug ) ) {
			return new \WP_Error( 'woocommerce_rest_invalid_plugin', __( 'Invalid plugin.', 'woocommerce' ), 404 );
		}

		$result = activate_plugin( PluginsHelper::get_plugin_path_from_slug( $plugin_slug ) );

		if ( ! is_null( $result ) ) {
			return new \WP_Error( 'woocommerce_rest_invalid_plugin', __( 'The plugin could not be activated.', 'woocommerce' ), 500 );
		}

		// IMPORTANT - Don't return the active plugins data here.
		// Instead we will get that data in a separate request to ensure they are loaded.
		return rest_ensure_response(
			array(
				'status' => 'success',
			)
		);
	}

	/**
	 * Check if a given request has access to manage plugins.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 *
	 * @return \WP_Error|boolean
	 */
	public function install_plugins_permissions_check( $request ) {
		if ( ! current_user_can( 'install_plugins' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage plugins.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Return installed marketing extensions data.
	 *
	 * @param \WP_REST_Request $request Request data.
	 *
	 * @return \WP_Error|\WP_REST_Response
	 */
	public function get_installed_plugins( $request ) {
		return rest_ensure_response( InstalledExtensions::get_data() );
	}

}
Admin/API/PaymentGatewaySuggestions.php000064400000012112152333011040014075 0ustar00<?php
/**
 * REST API Payment Gateway Suggestions Controller
 *
 * Handles requests to install and activate depedent plugins.
 */

namespace Automattic\WooCommerce\Admin\API;

use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\DefaultPaymentGateways;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\Init as Suggestions;

defined( 'ABSPATH' ) || exit;

/**
 * PaymentGatewaySuggetsions Controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class PaymentGatewaySuggestions extends \WC_REST_Data_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'payment-gateway-suggestions';

	/**
	 * Register routes.
	 */
	public function register_routes() {

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_suggestions' ),
					'permission_callback' => array( $this, 'get_permission_check' ),
					'args'                => array(
						'force_default_suggestions' => array(
							'type'        => 'boolean',
							'description' => __( 'Return the default payment suggestions when woocommerce_show_marketplace_suggestions and woocommerce_setting_payments_recommendations_hidden options are set to no', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/dismiss',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'dismiss_payment_gateway_suggestion' ),
					'permission_callback' => array( $this, 'get_permission_check' ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

	}

	/**
	 * Check if a given request has access to manage plugins.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_permission_check( $request ) {
		if ( ! current_user_can( 'install_plugins' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage plugins.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Return suggested payment gateways.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response
	 */
	public function get_suggestions( $request ) {

		$should_display = Suggestions::should_display();
		$force_default  = $request->get_param( 'force_default_suggestions' );

		if ( $should_display ) {
			return Suggestions::get_suggestions();
		} elseif ( false === $should_display && true === $force_default ) {
			return rest_ensure_response( Suggestions::get_suggestions( DefaultPaymentGateways::get_all() ) );
		}

		return rest_ensure_response( array() );
	}

	/**
	 * Dismisses suggested payment gateways.
	 *
	 * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response
	 */
	public function dismiss_payment_gateway_suggestion() {
		$success = Suggestions::dismiss();
		return rest_ensure_response( $success );
	}

	/**
	 * Get the schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'payment-gateway-suggestions',
			'type'       => 'array',
			'properties' => array(
				'content'                 => array(
					'description' => __( 'Suggestion description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'id'                      => array(
					'description' => __( 'Suggestion ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'image'                   => array(
					'description' => __( 'Gateway image.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'is_visible'              => array(
					'description' => __( 'Suggestion visibility.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'plugins'                 => array(
					'description' => __( 'Array of plugin slugs.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'recommendation_priority' => array(
					'description' => __( 'Priority of recommendation.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'title'                   => array(
					'description' => __( 'Gateway title.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
Admin/API/Reports/SqlQuery.php000064400000011706152333011040012136 0ustar00<?php
/**
 * Admin\API\Reports\SqlQuery class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Admin\API\Reports\SqlQuery: Common parent for manipulating SQL query clauses.
 */
class SqlQuery {
	/**
	 * List of SQL clauses.
	 *
	 * @var array
	 */
	private $sql_clauses = array(
		'select'     => array(),
		'from'       => array(),
		'left_join'  => array(),
		'join'       => array(),
		'right_join' => array(),
		'where'      => array(),
		'where_time' => array(),
		'group_by'   => array(),
		'having'     => array(),
		'limit'      => array(),
		'order_by'   => array(),
	);
	/**
	 * SQL clause merge filters.
	 *
	 * @var array
	 */
	private $sql_filters = array(
		'where' => array(
			'where',
			'where_time',
		),
		'join'  => array(
			'right_join',
			'join',
			'left_join',
		),
	);
	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context;

	/**
	 * Constructor.
	 *
	 * @param string $context Optional context passed to filters. Default empty string.
	 */
	public function __construct( $context = '' ) {
		$this->context = $context;
	}

	/**
	 * Add a SQL clause to be included when get_data is called.
	 *
	 * @param string $type   Clause type.
	 * @param string $clause SQL clause.
	 */
	protected function add_sql_clause( $type, $clause ) {
		if ( isset( $this->sql_clauses[ $type ] ) && ! empty( $clause ) ) {
			$this->sql_clauses[ $type ][] = $clause;
		}
	}

	/**
	 * Get SQL clause by type.
	 *
	 * @param string $type     Clause type.
	 * @param string $handling Whether to filter the return value (filtered|unfiltered). Default unfiltered.
	 *
	 * @return string SQL clause.
	 */
	protected function get_sql_clause( $type, $handling = 'unfiltered' ) {
		if ( ! isset( $this->sql_clauses[ $type ] ) ) {
			return '';
		}

		/**
		 * Default to bypassing filters for clause retrieval internal to data stores.
		 * The filters are applied when the full SQL statement is retrieved.
		 */
		if ( 'unfiltered' === $handling ) {
			return implode( ' ', $this->sql_clauses[ $type ] );
		}

		if ( isset( $this->sql_filters[ $type ] ) ) {
			$clauses = array();
			foreach ( $this->sql_filters[ $type ] as $subset ) {
				$clauses = array_merge( $clauses, $this->sql_clauses[ $subset ] );
			}
		} else {
			$clauses = $this->sql_clauses[ $type ];
		}

		/**
		 * Filter SQL clauses by type and context.
		 *
		 * @param array  $clauses The original arguments for the request.
		 * @param string $context The data store context.
		 */
		$clauses = apply_filters( "woocommerce_analytics_clauses_{$type}", $clauses, $this->context );
		/**
		 * Filter SQL clauses by type and context.
		 *
		 * @param array  $clauses The original arguments for the request.
		 */
		$clauses = apply_filters( "woocommerce_analytics_clauses_{$type}_{$this->context}", $clauses );
		return implode( ' ', $clauses );
	}

	/**
	 * Clear SQL clauses by type.
	 *
	 * @param string|array $types Clause type.
	 */
	protected function clear_sql_clause( $types ) {
		foreach ( (array) $types as $type ) {
			if ( isset( $this->sql_clauses[ $type ] ) ) {
				$this->sql_clauses[ $type ] = array();
			}
		}
	}

	/**
	 * Replace strings within SQL clauses by type.
	 *
	 * @param string $type    Clause type.
	 * @param string $search  String to search for.
	 * @param string $replace Replacement string.
	 */
	protected function str_replace_clause( $type, $search, $replace ) {
		if ( isset( $this->sql_clauses[ $type ] ) ) {
			foreach ( $this->sql_clauses[ $type ] as $key => $sql ) {
				$this->sql_clauses[ $type ][ $key ] = str_replace( $search, $replace, $sql );
			}
		}
	}

	/**
	 * Get the full SQL statement.
	 *
	 * @return string
	 */
	public function get_query_statement() {
		$join     = $this->get_sql_clause( 'join', 'filtered' );
		$where    = $this->get_sql_clause( 'where', 'filtered' );
		$group_by = $this->get_sql_clause( 'group_by', 'filtered' );
		$having   = $this->get_sql_clause( 'having', 'filtered' );
		$order_by = $this->get_sql_clause( 'order_by', 'filtered' );

		$statement = "
			SELECT
				{$this->get_sql_clause( 'select', 'filtered' )}
			FROM
				{$this->get_sql_clause( 'from', 'filtered' )}
				{$join}
			WHERE
				1=1
				{$where}
		";

		if ( ! empty( $group_by ) ) {
			$statement .= "
				GROUP BY
					{$group_by}
			";
			if ( ! empty( $having ) ) {
				$statement .= "
					HAVING
						1=1
						{$having}
				";
			}
		}

		if ( ! empty( $order_by ) ) {
			$statement .= "
				ORDER BY
					{$order_by}
			";
		}

		return $statement . $this->get_sql_clause( 'limit', 'filtered' );
	}

	/**
	 * Reinitialize the clause array.
	 */
	public function clear_all_clauses() {
		$this->sql_clauses = array(
			'select'     => array(),
			'from'       => array(),
			'left_join'  => array(),
			'join'       => array(),
			'right_join' => array(),
			'where'      => array(),
			'where_time' => array(),
			'group_by'   => array(),
			'having'     => array(),
			'limit'      => array(),
			'order_by'   => array(),
		);
	}
}
Admin/API/Reports/Customers/Stats/DataStore.php000064400000007232152333011040015320 0ustar00<?php
/**
 * API\Reports\Customers\Stats\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Customers\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;

/**
 * API\Reports\Customers\Stats\DataStore.
 */
class DataStore extends CustomersDataStore implements DataStoreInterface {
	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'customers_count'     => 'intval',
		'avg_orders_count'    => 'floatval',
		'avg_total_spend'     => 'floatval',
		'avg_avg_order_value' => 'floatval',
	);

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'customers_stats';

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'customers_stats';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$this->report_columns = array(
			'customers_count'     => 'COUNT( * ) as customers_count',
			'avg_orders_count'    => 'AVG( orders_count ) as avg_orders_count',
			'avg_total_spend'     => 'AVG( total_spend ) as avg_total_spend',
			'avg_avg_order_value' => 'AVG( avg_order_value ) as avg_avg_order_value',
		);
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$customers_table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page' => get_option( 'posts_per_page' ),
			'page'     => 1,
			'order'    => 'DESC',
			'orderby'  => 'date_registered',
			'fields'   => '*',
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$data = (object) array(
				'customers_count'     => 0,
				'avg_orders_count'    => 0,
				'avg_total_spend'     => 0.0,
				'avg_avg_order_value' => 0.0,
			);

			$selections = $this->selected_columns( $query_args );
			$this->add_sql_query_params( $query_args );
			// Clear SQL clauses set for parent class queries that are different here.
			$this->subquery->clear_sql_clause( 'select' );
			$this->subquery->add_sql_clause( 'select', 'SUM( total_sales ) AS total_spend,' );
			$this->subquery->add_sql_clause(
				'select',
				'SUM( CASE WHEN parent_id = 0 THEN 1 END ) as orders_count,'
			);
			$this->subquery->add_sql_clause(
				'select',
				'CASE WHEN SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) = 0 THEN NULL ELSE SUM( total_sales ) / SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) END AS avg_order_value'
			);

			$this->clear_sql_clause( array( 'order_by', 'limit' ) );
			$this->add_sql_clause( 'select', $selections );
			$this->add_sql_clause( 'from', "({$this->subquery->get_query_statement()}) AS tt" );

			$report_data = $wpdb->get_results(
				$this->get_query_statement(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				ARRAY_A
			);

			if ( null === $report_data ) {
				return $data;
			}

			$data = (object) $this->cast_numbers( $report_data[0] );

			$this->set_cached_data( $cache_key, $data );
		}

		return $data;
	}
}
Admin/API/Reports/Customers/Stats/Controller.php000064400000040531152333011040015554 0ustar00<?php
/**
 * REST API Reports customers stats controller
 *
 * Handles requests to the /reports/customers/stats endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Customers\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;

/**
 * REST API Reports customers stats controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/customers/stats';

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args                        = array();
		$args['registered_before']   = $request['registered_before'];
		$args['registered_after']    = $request['registered_after'];
		$args['match']               = $request['match'];
		$args['search']              = $request['search'];
		$args['name_includes']       = $request['name_includes'];
		$args['name_excludes']       = $request['name_excludes'];
		$args['username_includes']   = $request['username_includes'];
		$args['username_excludes']   = $request['username_excludes'];
		$args['email_includes']      = $request['email_includes'];
		$args['email_excludes']      = $request['email_excludes'];
		$args['country_includes']    = $request['country_includes'];
		$args['country_excludes']    = $request['country_excludes'];
		$args['last_active_before']  = $request['last_active_before'];
		$args['last_active_after']   = $request['last_active_after'];
		$args['orders_count_min']    = $request['orders_count_min'];
		$args['orders_count_max']    = $request['orders_count_max'];
		$args['total_spend_min']     = $request['total_spend_min'];
		$args['total_spend_max']     = $request['total_spend_max'];
		$args['avg_order_value_min'] = $request['avg_order_value_min'];
		$args['avg_order_value_max'] = $request['avg_order_value_max'];
		$args['last_order_before']   = $request['last_order_before'];
		$args['last_order_after']    = $request['last_order_after'];
		$args['customers']           = $request['customers'];
		$args['fields']              = $request['fields'];
		$args['force_cache_refresh'] = $request['force_cache_refresh'];

		$between_params_numeric    = array( 'orders_count', 'total_spend', 'avg_order_value' );
		$normalized_params_numeric = TimeInterval::normalize_between_params( $request, $between_params_numeric, false );
		$between_params_date       = array( 'last_active', 'registered' );
		$normalized_params_date    = TimeInterval::normalize_between_params( $request, $between_params_date, true );
		$args                      = array_merge( $args, $normalized_params_numeric, $normalized_params_date );

		return $args;
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$query_args      = $this->prepare_reports_query( $request );
		$customers_query = new Query( $query_args );
		$report_data     = $customers_query->get_data();
		$out_data        = array(
			'totals' => $report_data,
		);

		return rest_ensure_response( $out_data );
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param Array           $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_customers_stats', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		// @todo Should any of these be 'indicator's?
		$totals = array(
			'customers_count'     => array(
				'description' => __( 'Number of customers.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'avg_orders_count'    => array(
				'description' => __( 'Average number of orders.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'avg_total_spend'     => array(
				'description' => __( 'Average total spend per customer.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'format'      => 'currency',
			),
			'avg_avg_order_value' => array(
				'description' => __( 'Average AOV per customer.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'format'      => 'currency',
			),
		);

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_customers_stats',
			'type'       => 'object',
			'properties' => array(
				'totals' => array(
					'description' => __( 'Totals data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'properties'  => $totals,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                            = array();
		$params['context']                 = $this->get_context_param( array( 'default' => 'view' ) );
		$params['registered_before']       = array(
			'description'       => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['registered_after']        = array(
			'description'       => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['match']                   = array(
			'description'       => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'all',
			'enum'              => array(
				'all',
				'any',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['search']                  = array(
			'description'       => __( 'Limit response to objects with a customer field containing the search term. Searches the field provided by `searchby`.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['searchby']                = array(
			'description' => 'Limit results with `search` and `searchby` to specific fields containing the search term.',
			'type'        => 'string',
			'default'     => 'name',
			'enum'        => array(
				'name',
				'username',
				'email',
			),
		);
		$params['name_includes']           = array(
			'description'       => __( 'Limit response to objects with specfic names.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['name_excludes']           = array(
			'description'       => __( 'Limit response to objects excluding specfic names.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['username_includes']       = array(
			'description'       => __( 'Limit response to objects with specfic usernames.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['username_excludes']       = array(
			'description'       => __( 'Limit response to objects excluding specfic usernames.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['email_includes']          = array(
			'description'       => __( 'Limit response to objects including emails.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['email_excludes']          = array(
			'description'       => __( 'Limit response to objects excluding emails.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['country_includes']        = array(
			'description'       => __( 'Limit response to objects with specfic countries.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['country_excludes']        = array(
			'description'       => __( 'Limit response to objects excluding specfic countries.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['last_active_before']      = array(
			'description'       => __( 'Limit response to objects last active before (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['last_active_after']       = array(
			'description'       => __( 'Limit response to objects last active after (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['last_active_between']     = array(
			'description'       => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_date_arg' ),
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['registered_before']       = array(
			'description'       => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['registered_after']        = array(
			'description'       => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['registered_between']      = array(
			'description'       => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_date_arg' ),
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['orders_count_min']        = array(
			'description'       => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orders_count_max']        = array(
			'description'       => __( 'Limit response to objects with an order count less than or equal to given integer.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orders_count_between']    = array(
			'description'       => __( 'Limit response to objects with an order count between two given integers.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_numeric_arg' ),
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['total_spend_min']         = array(
			'description'       => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'woocommerce' ),
			'type'              => 'number',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['total_spend_max']         = array(
			'description'       => __( 'Limit response to objects with a total order spend less than or equal to given number.', 'woocommerce' ),
			'type'              => 'number',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['total_spend_between']     = array(
			'description'       => __( 'Limit response to objects with a total order spend between two given numbers.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_numeric_arg' ),
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['avg_order_value_min']     = array(
			'description'       => __( 'Limit response to objects with an average order spend greater than or equal to given number.', 'woocommerce' ),
			'type'              => 'number',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['avg_order_value_max']     = array(
			'description'       => __( 'Limit response to objects with an average order spend less than or equal to given number.', 'woocommerce' ),
			'type'              => 'number',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['avg_order_value_between'] = array(
			'description'       => __( 'Limit response to objects with an average order spend between two given numbers.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_numeric_arg' ),
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['last_order_before']       = array(
			'description'       => __( 'Limit response to objects with last order before (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['last_order_after']        = array(
			'description'       => __( 'Limit response to objects with last order after (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['customers']               = array(
			'description'       => __( 'Limit result to items with specified customer ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['fields']                  = array(
			'description'       => __( 'Limit stats fields to the specified items.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
Admin/API/Reports/Customers/Stats/Query.php000064400000003006152333011040014532 0ustar00<?php
/**
 * Class for parameter-based Customers Report Stats querying
 *
 * Example usage:
 * $args = array(
 *          'registered_before'   => '2018-07-19 00:00:00',
 *          'registered_after'    => '2018-07-05 00:00:00',
 *          'page'                => 2,
 *          'avg_order_value_min' => 100,
 *          'country'             => 'GB',
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Customers\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Customers\Stats\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Customers report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array(
			'per_page' => get_option( 'posts_per_page' ), // not sure if this should be the default.
			'page'     => 1,
			'order'    => 'DESC',
			'orderby'  => 'date_registered',
			'fields'   => '*', // @todo Needed?
		);
	}

	/**
	 * Get product data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_customers_stats_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-customers-stats' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_customers_stats_select_query', $results, $args );
	}
}
Admin/API/Reports/Customers/DataStore.php000064400000063440152333011040014225 0ustar00<?php
/**
 * Admin\API\Reports\Customers\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Customers;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
use Automattic\WooCommerce\Utilities\OrderUtil;

/**
 * Admin\API\Reports\Customers\DataStore.
 */
class DataStore extends ReportsDataStore implements DataStoreInterface {

	/**
	 * Table used to get the data.
	 *
	 * @var string
	 */
	protected static $table_name = 'wc_customer_lookup';

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'customers';

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'id'              => 'intval',
		'user_id'         => 'intval',
		'orders_count'    => 'intval',
		'total_spend'     => 'floatval',
		'avg_order_value' => 'floatval',
	);

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'customers';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		global $wpdb;
		$table_name           = self::get_db_table_name();
		$orders_count         = 'SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END )';
		$total_spend          = 'SUM( total_sales )';
		$this->report_columns = array(
			'id'               => "{$table_name}.customer_id as id",
			'user_id'          => 'user_id',
			'username'         => 'username',
			'name'             => "CONCAT_WS( ' ', first_name, last_name ) as name", // @xxx: What does this mean for RTL?
			'email'            => 'email',
			'country'          => 'country',
			'city'             => 'city',
			'state'            => 'state',
			'postcode'         => 'postcode',
			'date_registered'  => 'date_registered',
			'date_last_active' => 'IF( date_last_active <= "0000-00-00 00:00:00", NULL, date_last_active ) AS date_last_active',
			'date_last_order'  => "MAX( {$wpdb->prefix}wc_order_stats.date_created ) as date_last_order",
			'orders_count'     => "{$orders_count} as orders_count",
			'total_spend'      => "{$total_spend} as total_spend",
			'avg_order_value'  => "CASE WHEN {$orders_count} = 0 THEN NULL ELSE {$total_spend} / {$orders_count} END AS avg_order_value",
		);
	}

	/**
	 * Set up all the hooks for maintaining and populating table data.
	 */
	public static function init() {
		add_action( 'edit_user_profile_update', array( __CLASS__, 'update_registered_customer' ) );
		add_action( 'woocommerce_analytics_delete_order_stats', array( __CLASS__, 'sync_on_order_delete' ), 15, 2 );
	}

	/**
	 * Sync customers data after an order was deleted.
	 *
	 * When an order is deleted, the customer record is deleted from the
	 * table if the customer has no other orders.
	 *
	 * @param int $order_id Order ID.
	 * @param int $customer_id Customer ID.
	 */
	public static function sync_on_order_delete( $order_id, $customer_id ) {
		$customer_id = absint( $customer_id );

		if ( 0 === $customer_id ) {
			return;
		}

		// Calculate the amount of orders remaining for this customer.
		$order_count = self::get_order_count( $customer_id );

		if ( 0 === $order_count ) {
			self::delete_customer( $customer_id );
		}
	}

	/**
	 * Sync customers data after an order was updated.
	 *
	 * Only updates customer if it is the customers last order.
	 *
	 * @param int $post_id of order.
	 * @return true|-1
	 */
	public static function sync_order_customer( $post_id ) {
		global $wpdb;

		if ( ! OrderUtil::is_order( $post_id, array( 'shop_order', 'shop_order_refund' ) ) ) {
			return -1;
		}

		$order       = wc_get_order( $post_id );
		$customer_id = self::get_existing_customer_id_from_order( $order );
		if ( false === $customer_id ) {
			return -1;
		}
		$last_order = self::get_last_order( $customer_id );

		if ( ! $last_order || $order->get_id() !== $last_order->get_id() ) {
			return -1;
		}

		list($data, $format) = self::get_customer_order_data_and_format( $order );

		$result = $wpdb->update( self::get_db_table_name(), $data, array( 'customer_id' => $customer_id ), $format );

		/**
		 * Fires when a customer is updated.
		 *
		 * @param int $customer_id Customer ID.
		 * @since 4.0.0
		 */
		do_action( 'woocommerce_analytics_update_customer', $customer_id );

		return 1 === $result;
	}

	/**
	 * Maps ordering specified by the user to columns in the database/fields in the data.
	 *
	 * @param string $order_by Sorting criterion.
	 * @return string
	 */
	protected function normalize_order_by( $order_by ) {
		if ( 'name' === $order_by ) {
			return "CONCAT_WS( ' ', first_name, last_name )";
		}

		return $order_by;
	}

	/**
	 * Fills WHERE clause of SQL request with date-related constraints.
	 *
	 * @param array  $query_args Parameters supplied by the user.
	 * @param string $table_name Name of the db table relevant for the date constraint.
	 */
	protected function add_time_period_sql_params( $query_args, $table_name ) {
		global $wpdb;

		$this->clear_sql_clause( array( 'where', 'where_time', 'having' ) );
		$date_param_mapping  = array(
			'registered'  => array(
				'clause' => 'where',
				'column' => $table_name . '.date_registered',
			),
			'order'       => array(
				'clause' => 'where',
				'column' => $wpdb->prefix . 'wc_order_stats.date_created',
			),
			'last_active' => array(
				'clause' => 'where',
				'column' => $table_name . '.date_last_active',
			),
			'last_order'  => array(
				'clause' => 'having',
				'column' => "MAX( {$wpdb->prefix}wc_order_stats.date_created )",
			),
		);
		$match_operator      = $this->get_match_operator( $query_args );
		$where_time_clauses  = array();
		$having_time_clauses = array();

		foreach ( $date_param_mapping as $query_param => $param_info ) {
			$subclauses  = array();
			$before_arg  = $query_param . '_before';
			$after_arg   = $query_param . '_after';
			$column_name = $param_info['column'];

			if ( ! empty( $query_args[ $before_arg ] ) ) {
				$datetime     = new \DateTime( $query_args[ $before_arg ] );
				$datetime_str = $datetime->format( TimeInterval::$sql_datetime_format );
				$subclauses[] = "{$column_name} <= '$datetime_str'";
			}

			if ( ! empty( $query_args[ $after_arg ] ) ) {
				$datetime     = new \DateTime( $query_args[ $after_arg ] );
				$datetime_str = $datetime->format( TimeInterval::$sql_datetime_format );
				$subclauses[] = "{$column_name} >= '$datetime_str'";
			}

			if ( $subclauses && ( 'where' === $param_info['clause'] ) ) {
				$where_time_clauses[] = '(' . implode( ' AND ', $subclauses ) . ')';
			}

			if ( $subclauses && ( 'having' === $param_info['clause'] ) ) {
				$having_time_clauses[] = '(' . implode( ' AND ', $subclauses ) . ')';
			}
		}

		if ( $where_time_clauses ) {
			$this->subquery->add_sql_clause( 'where_time', 'AND ' . implode( " {$match_operator} ", $where_time_clauses ) );
		}

		if ( $having_time_clauses ) {
			$this->subquery->add_sql_clause( 'having', 'AND ' . implode( " {$match_operator} ", $having_time_clauses ) );
		}
	}

	/**
	 * Updates the database query with parameters used for Customers report: categories and order status.
	 *
	 * @param array $query_args Query arguments supplied by the user.
	 */
	protected function add_sql_query_params( $query_args ) {
		global $wpdb;
		$customer_lookup_table  = self::get_db_table_name();
		$order_stats_table_name = $wpdb->prefix . 'wc_order_stats';

		$this->add_time_period_sql_params( $query_args, $customer_lookup_table );
		$this->get_limit_sql_params( $query_args );
		$this->add_order_by_sql_params( $query_args );
		$this->subquery->add_sql_clause( 'left_join', "LEFT JOIN {$order_stats_table_name} ON {$customer_lookup_table}.customer_id = {$order_stats_table_name}.customer_id" );

		$match_operator = $this->get_match_operator( $query_args );
		$where_clauses  = array();
		$having_clauses = array();

		$exact_match_params = array(
			'name',
			'username',
			'email',
			'country',
		);

		foreach ( $exact_match_params as $exact_match_param ) {
			if ( ! empty( $query_args[ $exact_match_param . '_includes' ] ) ) {
				$exact_match_arguments         = $query_args[ $exact_match_param . '_includes' ];
				$exact_match_arguments_escaped = array_map( 'esc_sql', explode( ',', $exact_match_arguments ) );
				$included                      = implode( "','", $exact_match_arguments_escaped );
				// 'country_includes' is a list of country codes, the others will be a list of customer ids.
				$table_column    = 'country' === $exact_match_param ? $exact_match_param : 'customer_id';
				$where_clauses[] = "{$customer_lookup_table}.{$table_column} IN ('{$included}')";
			}

			if ( ! empty( $query_args[ $exact_match_param . '_excludes' ] ) ) {
				$exact_match_arguments         = $query_args[ $exact_match_param . '_excludes' ];
				$exact_match_arguments_escaped = array_map( 'esc_sql', explode( ',', $exact_match_arguments ) );
				$excluded                      = implode( "','", $exact_match_arguments_escaped );
				// 'country_includes' is a list of country codes, the others will be a list of customer ids.
				$table_column    = 'country' === $exact_match_param ? $exact_match_param : 'customer_id';
				$where_clauses[] = "{$customer_lookup_table}.{$table_column} NOT IN ('{$excluded}')";
			}
		}

		$search_params = array(
			'name',
			'username',
			'email',
		);

		if ( ! empty( $query_args['search'] ) ) {
			$name_like = '%' . $wpdb->esc_like( $query_args['search'] ) . '%';

			if ( empty( $query_args['searchby'] ) || 'name' === $query_args['searchby'] || ! in_array( $query_args['searchby'], $search_params, true ) ) {
				$searchby = "CONCAT_WS( ' ', first_name, last_name )";
			} else {
				$searchby = $query_args['searchby'];
			}

			$where_clauses[] = $wpdb->prepare( "{$searchby} LIKE %s", $name_like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		}

		// Allow a list of customer IDs to be specified.
		if ( ! empty( $query_args['customers'] ) ) {
			$included_customers = $this->get_filtered_ids( $query_args, 'customers' );
			$where_clauses[]    = "{$customer_lookup_table}.customer_id IN ({$included_customers})";
		}

		// Allow a list of user IDs to be specified.
		if ( ! empty( $query_args['users'] ) ) {
			$included_users  = $this->get_filtered_ids( $query_args, 'users' );
			$where_clauses[] = "{$customer_lookup_table}.user_id IN ({$included_users})";
		}

		$numeric_params = array(
			'orders_count'    => array(
				'column' => 'COUNT( order_id )',
				'format' => '%d',
			),
			'total_spend'     => array(
				'column' => 'SUM( total_sales )',
				'format' => '%f',
			),
			'avg_order_value' => array(
				'column' => '( SUM( total_sales ) / COUNT( order_id ) )',
				'format' => '%f',
			),
		);

		foreach ( $numeric_params as $numeric_param => $param_info ) {
			$subclauses = array();
			$min_param  = $numeric_param . '_min';
			$max_param  = $numeric_param . '_max';
			$or_equal   = isset( $query_args[ $min_param ] ) && isset( $query_args[ $max_param ] ) ? '=' : '';

			if ( isset( $query_args[ $min_param ] ) ) {
				$subclauses[] = $wpdb->prepare(
					// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
					"{$param_info['column']} >{$or_equal} {$param_info['format']}",
					$query_args[ $min_param ]
				);
			}

			if ( isset( $query_args[ $max_param ] ) ) {
				$subclauses[] = $wpdb->prepare(
					// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
					"{$param_info['column']} <{$or_equal} {$param_info['format']}",
					$query_args[ $max_param ]
				);
			}

			if ( $subclauses ) {
				$having_clauses[] = '(' . implode( ' AND ', $subclauses ) . ')';
			}
		}

		if ( $where_clauses ) {
			$preceding_match = empty( $this->get_sql_clause( 'where_time' ) ) ? ' AND ' : " {$match_operator} ";
			$this->subquery->add_sql_clause( 'where', $preceding_match . implode( " {$match_operator} ", $where_clauses ) );
		}

		$order_status_filter = $this->get_status_subquery( $query_args );
		if ( $order_status_filter ) {
			$this->subquery->add_sql_clause( 'left_join', "AND ( {$order_status_filter} )" );
		}

		if ( $having_clauses ) {
			$preceding_match = empty( $this->get_sql_clause( 'having' ) ) ? ' AND ' : " {$match_operator} ";
			$this->subquery->add_sql_clause( 'having', $preceding_match . implode( " {$match_operator} ", $having_clauses ) );
		}
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$customers_table_name   = self::get_db_table_name();
		$order_stats_table_name = $wpdb->prefix . 'wc_order_stats';

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page'     => get_option( 'posts_per_page' ),
			'page'         => 1,
			'order'        => 'DESC',
			'orderby'      => 'date_registered',
			'order_before' => TimeInterval::default_before(),
			'order_after'  => TimeInterval::default_after(),
			'fields'       => '*',
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$data = (object) array(
				'data'    => array(),
				'total'   => 0,
				'pages'   => 0,
				'page_no' => 0,
			);

			$selections       = $this->selected_columns( $query_args );
			$sql_query_params = $this->add_sql_query_params( $query_args );
			$count_query      = "SELECT COUNT(*) FROM (
					{$this->subquery->get_query_statement()}
				) as tt
				";
			$db_records_count = (int) $wpdb->get_var(
				$count_query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			);

			$params      = $this->get_limit_params( $query_args );
			$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
			if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
				return $data;
			}

			$this->subquery->clear_sql_clause( 'select' );
			$this->subquery->add_sql_clause( 'select', $selections );
			$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
			$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );

			$customer_data = $wpdb->get_results(
				$this->subquery->get_query_statement(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				ARRAY_A
			);

			if ( null === $customer_data ) {
				return $data;
			}

			$customer_data = array_map( array( $this, 'cast_numbers' ), $customer_data );
			$data          = (object) array(
				'data'    => $customer_data,
				'total'   => $db_records_count,
				'pages'   => $total_pages,
				'page_no' => (int) $query_args['page'],
			);

			$this->set_cached_data( $cache_key, $data );
		}

		return $data;
	}

	/**
	 * Returns an existing customer ID for an order if one exists.
	 *
	 * @param object $order WC Order.
	 * @return int|bool
	 */
	public static function get_existing_customer_id_from_order( $order ) {
		global $wpdb;

		if ( ! is_a( $order, 'WC_Order' ) ) {
			return false;
		}

		$user_id = $order->get_customer_id();

		if ( 0 === $user_id ) {
			$customer_id = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT customer_id FROM {$wpdb->prefix}wc_order_stats WHERE order_id = %d",
					$order->get_id()
				)
			);

			if ( $customer_id ) {
				return $customer_id;
			}

			$email = $order->get_billing_email( 'edit' );

			if ( $email ) {
				return self::get_guest_id_by_email( $email );
			} else {
				return false;
			}
		} else {
			return self::get_customer_id_by_user_id( $user_id );
		}
	}

	/**
	 * Get or create a customer from a given order.
	 *
	 * @param object $order WC Order.
	 * @return int|bool
	 */
	public static function get_or_create_customer_from_order( $order ) {
		if ( ! $order ) {
			return false;
		}

		global $wpdb;

		if ( ! is_a( $order, 'WC_Order' ) ) {
			return false;
		}

		$returning_customer_id = self::get_existing_customer_id_from_order( $order );

		if ( $returning_customer_id ) {
			return $returning_customer_id;
		}

		list($data, $format) = self::get_customer_order_data_and_format( $order );

		$result      = $wpdb->insert( self::get_db_table_name(), $data, $format );
		$customer_id = $wpdb->insert_id;

		/**
		 * Fires when a new report customer is created.
		 *
		 * @param int $customer_id Customer ID.
		 * @since 4.0.0
		 */
		do_action( 'woocommerce_analytics_new_customer', $customer_id );

		return $result ? $customer_id : false;
	}

	/**
	 * Returns a data object and format object of the customers data coming from the order.
	 *
	 * @param object      $order         WC_Order where we get customer info from.
	 * @param object|null $customer_user WC_Customer registered customer WP user.
	 * @return array ($data, $format)
	 */
	public static function get_customer_order_data_and_format( $order, $customer_user = null ) {
		$data   = array(
			'first_name'       => $order->get_customer_first_name(),
			'last_name'        => $order->get_customer_last_name(),
			'email'            => $order->get_billing_email( 'edit' ),
			'city'             => $order->get_billing_city( 'edit' ),
			'state'            => $order->get_billing_state( 'edit' ),
			'postcode'         => $order->get_billing_postcode( 'edit' ),
			'country'          => $order->get_billing_country( 'edit' ),
			'date_last_active' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
		);
		$format = array(
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
		);

		// Add registered customer data.
		if ( 0 !== $order->get_user_id() ) {
			$user_id = $order->get_user_id();
			if ( is_null( $customer_user ) ) {
				$customer_user = new \WC_Customer( $user_id );
			}
			$data['user_id']         = $user_id;
			$data['username']        = $customer_user->get_username( 'edit' );
			$data['date_registered'] = $customer_user->get_date_created( 'edit' ) ? $customer_user->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ) : null;
			$format[]                = '%d';
			$format[]                = '%s';
			$format[]                = '%s';
		}
		return array( $data, $format );
	}

	/**
	 * Retrieve a guest ID (when user_id is null) by email.
	 *
	 * @param string $email Email address.
	 * @return false|array Customer array if found, boolean false if not.
	 */
	public static function get_guest_id_by_email( $email ) {
		global $wpdb;

		$table_name  = self::get_db_table_name();
		$customer_id = $wpdb->get_var(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT customer_id FROM {$table_name} WHERE email = %s AND user_id IS NULL LIMIT 1",
				$email
			)
		);

		return $customer_id ? (int) $customer_id : false;
	}

	/**
	 * Retrieve a registered customer row id by user_id.
	 *
	 * @param string|int $user_id User ID.
	 * @return false|int Customer ID if found, boolean false if not.
	 */
	public static function get_customer_id_by_user_id( $user_id ) {
		global $wpdb;

		$table_name  = self::get_db_table_name();
		$customer_id = $wpdb->get_var(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT customer_id FROM {$table_name} WHERE user_id = %d LIMIT 1",
				$user_id
			)
		);

		return $customer_id ? (int) $customer_id : false;
	}

	/**
	 * Retrieve the last order made by a customer.
	 *
	 * @param int $customer_id Customer ID.
	 * @return object WC_Order|false.
	 */
	public static function get_last_order( $customer_id ) {
		global $wpdb;
		$orders_table = $wpdb->prefix . 'wc_order_stats';

		$last_order = $wpdb->get_var(
			$wpdb->prepare(
				// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT order_id, date_created_gmt FROM {$orders_table}
				WHERE customer_id = %d
				ORDER BY date_created_gmt DESC, order_id DESC LIMIT 1",
				// phpcs:enable
				$customer_id
			)
		);
		if ( ! $last_order ) {
			return false;
		}
		return wc_get_order( absint( $last_order ) );
	}

	/**
	 * Retrieve the oldest orders made by a customer.
	 *
	 * @param int $customer_id Customer ID.
	 * @return array Orders.
	 */
	public static function get_oldest_orders( $customer_id ) {
		global $wpdb;
		$orders_table                = $wpdb->prefix . 'wc_order_stats';
		$excluded_statuses           = array_map( array( __CLASS__, 'normalize_order_status' ), self::get_excluded_report_order_statuses() );
		$excluded_statuses_condition = '';
		if ( ! empty( $excluded_statuses ) ) {
			$excluded_statuses_str       = implode( "','", $excluded_statuses );
			$excluded_statuses_condition = "AND status NOT IN ('{$excluded_statuses_str}')";
		}

		return $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT order_id, date_created FROM {$orders_table} WHERE customer_id = %d {$excluded_statuses_condition} ORDER BY date_created, order_id ASC LIMIT 2",
				$customer_id
			)
		);
	}

	/**
	 * Retrieve the amount of orders made by a customer.
	 *
	 * @param int $customer_id Customer ID.
	 * @return int|null Amount of orders for customer or null on failure.
	 */
	public static function get_order_count( $customer_id ) {
		global $wpdb;
		$customer_id = absint( $customer_id );

		if ( 0 === $customer_id ) {
			return null;
		}

		$result = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT( order_id ) FROM {$wpdb->prefix}wc_order_stats WHERE customer_id = %d",
				$customer_id
			)
		);

		if ( is_null( $result ) ) {
			return null;
		}

		return (int) $result;
	}

	/**
	 * Update the database with customer data.
	 *
	 * @param int $user_id WP User ID to update customer data for.
	 * @return int|bool|null Number or rows modified or false on failure.
	 */
	public static function update_registered_customer( $user_id ) {
		global $wpdb;

		$customer = new \WC_Customer( $user_id );

		if ( ! self::is_valid_customer( $user_id ) ) {
			return false;
		}

		$first_name = $customer->get_first_name();
		$last_name  = $customer->get_last_name();

		if ( empty( $first_name ) ) {
			$first_name = $customer->get_billing_first_name();
		}
		if ( empty( $last_name ) ) {
			$last_name = $customer->get_billing_last_name();
		}

		$last_active = $customer->get_meta( 'wc_last_active', true, 'edit' );
		$data        = array(
			'user_id'          => $user_id,
			'username'         => $customer->get_username( 'edit' ),
			'first_name'       => $first_name,
			'last_name'        => $last_name,
			'email'            => $customer->get_email( 'edit' ),
			'city'             => $customer->get_billing_city( 'edit' ),
			'state'            => $customer->get_billing_state( 'edit' ),
			'postcode'         => $customer->get_billing_postcode( 'edit' ),
			'country'          => $customer->get_billing_country( 'edit' ),
			'date_registered'  => $customer->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ),
			'date_last_active' => $last_active ? gmdate( 'Y-m-d H:i:s', $last_active ) : null,
		);
		$format      = array(
			'%d',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
		);

		$customer_id = self::get_customer_id_by_user_id( $user_id );

		if ( $customer_id ) {
			// Preserve customer_id for existing user_id.
			$data['customer_id'] = $customer_id;
			$format[]            = '%d';
		}

		$results = $wpdb->replace( self::get_db_table_name(), $data, $format );

		/**
		 * Fires when customser's reports are updated.
		 *
		 * @param int $customer_id Customer ID.
		 * @since 4.0.0
		 */
		do_action( 'woocommerce_analytics_update_customer', $customer_id );

		ReportsCache::invalidate();

		return $results;
	}

	/**
	 * Check if a user ID is a valid customer or other user role with past orders.
	 *
	 * @param int $user_id User ID.
	 * @return bool
	 */
	protected static function is_valid_customer( $user_id ) {
		$user = new \WP_User( $user_id );

		if ( (int) $user_id !== $user->ID ) {
			return false;
		}

		/**
		 * Filter the customer roles, used to check if the user is a customer.
		 *
		 * @param array List of customer roles.
		 * @since 4.0.0
		 */
		$customer_roles = (array) apply_filters( 'woocommerce_analytics_customer_roles', array( 'customer' ) );

		if ( empty( $user->roles ) || empty( array_intersect( $user->roles, $customer_roles ) ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Delete a customer lookup row.
	 *
	 * @param int $customer_id Customer ID.
	 */
	public static function delete_customer( $customer_id ) {
		global $wpdb;

		$customer_id = (int) $customer_id;
		$num_deleted = $wpdb->delete( self::get_db_table_name(), array( 'customer_id' => $customer_id ) );

		if ( $num_deleted ) {
			/**
			 * Fires when a customer is deleted.
			 *
			 * @param int $order_id Order ID.
			 * @since 4.0.0
			 */
			do_action( 'woocommerce_analytics_delete_customer', $customer_id );

			ReportsCache::invalidate();
		}
	}

	/**
	 * Delete a customer lookup row by WordPress User ID.
	 *
	 * @param int $user_id WordPress User ID.
	 */
	public static function delete_customer_by_user_id( $user_id ) {
		global $wpdb;

		$user_id     = (int) $user_id;
		$num_deleted = $wpdb->delete( self::get_db_table_name(), array( 'user_id' => $user_id ) );

		if ( $num_deleted ) {
			ReportsCache::invalidate();
		}
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		$table_name     = self::get_db_table_name();
		$this->subquery = new SqlQuery( $this->context . '_subquery' );
		$this->subquery->add_sql_clause( 'from', $table_name );
		$this->subquery->add_sql_clause( 'select', "{$table_name}.customer_id" );
		$this->subquery->add_sql_clause( 'group_by', "{$table_name}.customer_id" );
	}
}
Admin/API/Reports/Customers/Controller.php000064400000063363152333011040014466 0ustar00<?php
/**
 * REST API Reports customers controller
 *
 * Handles requests to the /reports/customers endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Customers;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;

/**
 * REST API Reports customers controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
	/**
	 * Exportable traits.
	 */
	use ExportableTraits;

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/customers';

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args                        = array();
		$args['registered_before']   = $request['registered_before'];
		$args['registered_after']    = $request['registered_after'];
		$args['order_before']        = $request['before'];
		$args['order_after']         = $request['after'];
		$args['page']                = $request['page'];
		$args['per_page']            = $request['per_page'];
		$args['order']               = $request['order'];
		$args['orderby']             = $request['orderby'];
		$args['match']               = $request['match'];
		$args['search']              = $request['search'];
		$args['searchby']            = $request['searchby'];
		$args['name_includes']       = $request['name_includes'];
		$args['name_excludes']       = $request['name_excludes'];
		$args['username_includes']   = $request['username_includes'];
		$args['username_excludes']   = $request['username_excludes'];
		$args['email_includes']      = $request['email_includes'];
		$args['email_excludes']      = $request['email_excludes'];
		$args['country_includes']    = $request['country_includes'];
		$args['country_excludes']    = $request['country_excludes'];
		$args['last_active_before']  = $request['last_active_before'];
		$args['last_active_after']   = $request['last_active_after'];
		$args['orders_count_min']    = $request['orders_count_min'];
		$args['orders_count_max']    = $request['orders_count_max'];
		$args['total_spend_min']     = $request['total_spend_min'];
		$args['total_spend_max']     = $request['total_spend_max'];
		$args['avg_order_value_min'] = $request['avg_order_value_min'];
		$args['avg_order_value_max'] = $request['avg_order_value_max'];
		$args['last_order_before']   = $request['last_order_before'];
		$args['last_order_after']    = $request['last_order_after'];
		$args['customers']           = $request['customers'];
		$args['users']               = $request['users'];
		$args['force_cache_refresh'] = $request['force_cache_refresh'];

		$between_params_numeric    = array( 'orders_count', 'total_spend', 'avg_order_value' );
		$normalized_params_numeric = TimeInterval::normalize_between_params( $request, $between_params_numeric, false );
		$between_params_date       = array( 'last_active', 'registered' );
		$normalized_params_date    = TimeInterval::normalize_between_params( $request, $between_params_date, true );
		$args                      = array_merge( $args, $normalized_params_numeric, $normalized_params_date );

		return $args;
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$query_args      = $this->prepare_reports_query( $request );
		$customers_query = new Query( $query_args );
		$report_data     = $customers_query->get_data();

		$data = array();

		foreach ( $report_data->data as $customer_data ) {
			$item   = $this->prepare_item_for_response( $customer_data, $request );
			$data[] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		$page      = $report_data->page_no;
		$max_pages = $report_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}


	/**
	 * Get one report.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_item( $request ) {
		$query_args              = $this->prepare_reports_query( $request );
		$query_args['customers'] = array( $request->get_param( 'id' ) );
		$customers_query         = new Query( $query_args );
		$report_data             = $customers_query->get_data();

		$data = array();

		foreach ( $report_data->data as $customer_data ) {
			$item   = $this->prepare_item_for_response( $customer_data, $request );
			$data[] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param array           $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $report, $request );
		// Registered date is UTC.
		$data['date_registered_gmt'] = wc_rest_prepare_date_response( $data['date_registered'] );
		$data['date_registered']     = wc_rest_prepare_date_response( $data['date_registered'], false );
		// Last active date is local time.
		$data['date_last_active_gmt'] = wc_rest_prepare_date_response( $data['date_last_active'], false );
		$data['date_last_active']     = wc_rest_prepare_date_response( $data['date_last_active'] );
		$data                         = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $report ) );
		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 * @since 4.0.0
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_customers', $response, $report, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param array $object Object data.
	 * @return array
	 */
	protected function prepare_links( $object ) {
		if ( empty( $object['user_id'] ) ) {
			return array();
		}

		return array(
			'customer'   => array(
				'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object['id'] ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/customers', $this->namespace ) ),
			),
		);
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_customers',
			'type'       => 'object',
			'properties' => array(
				'id'                   => array(
					'description' => __( 'Customer ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'user_id'              => array(
					'description' => __( 'User ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'                 => array(
					'description' => __( 'Name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'username'             => array(
					'description' => __( 'Username.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'country'              => array(
					'description' => __( 'Country / Region.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'city'                 => array(
					'description' => __( 'City.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'state'                => array(
					'description' => __( 'Region.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'postcode'             => array(
					'description' => __( 'Postal code.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_registered'      => array(
					'description' => __( 'Date registered.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_registered_gmt'  => array(
					'description' => __( 'Date registered GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_last_active'     => array(
					'description' => __( 'Date last active.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_last_active_gmt' => array(
					'description' => __( 'Date last active GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'orders_count'         => array(
					'description' => __( 'Order count.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total_spend'          => array(
					'description' => __( 'Total spend.', 'woocommerce' ),
					'type'        => 'number',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'avg_order_value'      => array(
					'description' => __( 'Avg order value.', 'woocommerce' ),
					'type'        => 'number',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);
		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                            = array();
		$params['context']                 = $this->get_context_param( array( 'default' => 'view' ) );
		$params['registered_before']       = array(
			'description'       => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['registered_after']        = array(
			'description'       => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']                   = array(
			'description'       => __( 'Limit response to resources with orders published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']                  = array(
			'description'       => __( 'Limit response to resources with orders published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['page']                    = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']                = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']                   = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']                 = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date_registered',
			'enum'              => array(
				'username',
				'name',
				'country',
				'city',
				'state',
				'postcode',
				'date_registered',
				'date_last_active',
				'orders_count',
				'total_spend',
				'avg_order_value',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['match']                   = array(
			'description'       => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'all',
			'enum'              => array(
				'all',
				'any',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['search']                  = array(
			'description'       => __( 'Limit response to objects with a customer field containing the search term. Searches the field provided by `searchby`.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['searchby']                = array(
			'description' => 'Limit results with `search` and `searchby` to specific fields containing the search term.',
			'type'        => 'string',
			'default'     => 'name',
			'enum'        => array(
				'name',
				'username',
				'email',
			),
		);
		$params['name_includes']           = array(
			'description'       => __( 'Limit response to objects with specfic names.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['name_excludes']           = array(
			'description'       => __( 'Limit response to objects excluding specfic names.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['username_includes']       = array(
			'description'       => __( 'Limit response to objects with specfic usernames.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['username_excludes']       = array(
			'description'       => __( 'Limit response to objects excluding specfic usernames.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['email_includes']          = array(
			'description'       => __( 'Limit response to objects including emails.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['email_excludes']          = array(
			'description'       => __( 'Limit response to objects excluding emails.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['country_includes']        = array(
			'description'       => __( 'Limit response to objects with specfic countries.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['country_excludes']        = array(
			'description'       => __( 'Limit response to objects excluding specfic countries.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['last_active_before']      = array(
			'description'       => __( 'Limit response to objects last active before (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['last_active_after']       = array(
			'description'       => __( 'Limit response to objects last active after (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['last_active_between']     = array(
			'description'       => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_date_arg' ),
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['registered_before']       = array(
			'description'       => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['registered_after']        = array(
			'description'       => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['registered_between']      = array(
			'description'       => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_date_arg' ),
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['orders_count_min']        = array(
			'description'       => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orders_count_max']        = array(
			'description'       => __( 'Limit response to objects with an order count less than or equal to given integer.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orders_count_between']    = array(
			'description'       => __( 'Limit response to objects with an order count between two given integers.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_numeric_arg' ),
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['total_spend_min']         = array(
			'description'       => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'woocommerce' ),
			'type'              => 'number',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['total_spend_max']         = array(
			'description'       => __( 'Limit response to objects with a total order spend less than or equal to given number.', 'woocommerce' ),
			'type'              => 'number',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['total_spend_between']     = array(
			'description'       => __( 'Limit response to objects with a total order spend between two given numbers.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_numeric_arg' ),
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['avg_order_value_min']     = array(
			'description'       => __( 'Limit response to objects with an average order spend greater than or equal to given number.', 'woocommerce' ),
			'type'              => 'number',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['avg_order_value_max']     = array(
			'description'       => __( 'Limit response to objects with an average order spend less than or equal to given number.', 'woocommerce' ),
			'type'              => 'number',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['avg_order_value_between'] = array(
			'description'       => __( 'Limit response to objects with an average order spend between two given numbers.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_numeric_arg' ),
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['last_order_before']       = array(
			'description'       => __( 'Limit response to objects with last order before (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['last_order_after']        = array(
			'description'       => __( 'Limit response to objects with last order after (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['customers']               = array(
			'description'       => __( 'Limit result to items with specified customer ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['users']                   = array(
			'description'       => __( 'Limit result to items with specified user ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get the column names for export.
	 *
	 * @return array Key value pair of Column ID => Label.
	 */
	public function get_export_columns() {
		$export_columns = array(
			'name'            => __( 'Name', 'woocommerce' ),
			'username'        => __( 'Username', 'woocommerce' ),
			'last_active'     => __( 'Last Active', 'woocommerce' ),
			'registered'      => __( 'Sign Up', 'woocommerce' ),
			'email'           => __( 'Email', 'woocommerce' ),
			'orders_count'    => __( 'Orders', 'woocommerce' ),
			'total_spend'     => __( 'Total Spend', 'woocommerce' ),
			'avg_order_value' => __( 'AOV', 'woocommerce' ),
			'country'         => __( 'Country / Region', 'woocommerce' ),
			'city'            => __( 'City', 'woocommerce' ),
			'region'          => __( 'Region', 'woocommerce' ),
			'postcode'        => __( 'Postal Code', 'woocommerce' ),
		);

		/**
		 * Filter to add or remove column names from the customers report for
		 * export.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_report_customers_export_columns',
			$export_columns
		);
	}

	/**
	 * Get the column values for export.
	 *
	 * @param array $item Single report item/row.
	 * @return array Key value pair of Column ID => Row Value.
	 */
	public function prepare_item_for_export( $item ) {
		$export_item = array(
			'name'            => $item['name'],
			'username'        => $item['username'],
			'last_active'     => $item['date_last_active'],
			'registered'      => $item['date_registered'],
			'email'           => $item['email'],
			'orders_count'    => $item['orders_count'],
			'total_spend'     => self::csv_number_format( $item['total_spend'] ),
			'avg_order_value' => self::csv_number_format( $item['avg_order_value'] ),
			'country'         => $item['country'],
			'city'            => $item['city'],
			'region'          => $item['state'],
			'postcode'        => $item['postcode'],
		);

		/**
		 * Filter the column values of an item being exported.
		 *
		 * @param object $export_item Key value pair of Column ID => Row Value.
		 * @param object $item        Single report item/row.
		 * @since 4.0.0
		 */
		return apply_filters(
			'woocommerce_report_customers_prepare_export_item',
			$export_item,
			$item
		);
	}
}
Admin/API/Reports/Customers/Query.php000064400000002713152333011040013440 0ustar00<?php
/**
 * Class for parameter-based Customers Report querying
 *
 * Example usage:
 * $args = array(
 *          'registered_before'   => '2018-07-19 00:00:00',
 *          'registered_after'    => '2018-07-05 00:00:00',
 *          'page'                => 2,
 *          'avg_order_value_min' => 100,
 *          'country'             => 'GB',
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Customers\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Customers;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Customers\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Customers report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array(
			'per_page' => get_option( 'posts_per_page' ), // not sure if this should be the default.
			'page'     => 1,
			'order'    => 'DESC',
			'orderby'  => 'date_registered',
			'fields'   => '*',
		);
	}

	/**
	 * Get product data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_customers_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-customers' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_customers_select_query', $results, $args );
	}
}
Admin/API/Reports/Taxes/DataStore.php000064400000025753152333011040013332 0ustar00<?php
/**
 * API\Reports\Taxes\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Taxes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;

/**
 * API\Reports\Taxes\DataStore.
 */
class DataStore extends ReportsDataStore implements DataStoreInterface {

	/**
	 * Table used to get the data.
	 *
	 * @var string
	 */
	protected static $table_name = 'wc_order_tax_lookup';

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'taxes';

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'tax_rate_id'  => 'intval',
		'name'         => 'strval',
		'tax_rate'     => 'floatval',
		'country'      => 'strval',
		'state'        => 'strval',
		'priority'     => 'intval',
		'total_tax'    => 'floatval',
		'order_tax'    => 'floatval',
		'shipping_tax' => 'floatval',
		'orders_count' => 'intval',
	);

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'taxes';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$table_name           = self::get_db_table_name();
		$this->report_columns = array(
			'tax_rate_id'  => "{$table_name}.tax_rate_id",
			'name'         => 'tax_rate_name as name',
			'tax_rate'     => 'tax_rate',
			'country'      => 'tax_rate_country as country',
			'state'        => 'tax_rate_state as state',
			'priority'     => 'tax_rate_priority as priority',
			'total_tax'    => 'SUM(total_tax) as total_tax',
			'order_tax'    => 'SUM(order_tax) as order_tax',
			'shipping_tax' => 'SUM(shipping_tax) as shipping_tax',
			'orders_count' => "COUNT( DISTINCT ( CASE WHEN total_tax >= 0 THEN {$table_name}.order_id END ) ) as orders_count",
		);
	}

	/**
	 * Set up all the hooks for maintaining and populating table data.
	 */
	public static function init() {
		add_action( 'woocommerce_analytics_delete_order_stats', array( __CLASS__, 'sync_on_order_delete' ), 15 );
	}

	/**
	 * Fills FROM clause of SQL request based on user supplied parameters.
	 *
	 * @param array  $query_args          Query arguments supplied by the user.
	 * @param string $order_status_filter Order status subquery.
	 */
	protected function add_from_sql_params( $query_args, $order_status_filter ) {
		global $wpdb;
		$table_name = self::get_db_table_name();

		if ( $order_status_filter ) {
			$this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->prefix}wc_order_stats ON {$table_name}.order_id = {$wpdb->prefix}wc_order_stats.order_id" );
		}

		if ( isset( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
			$this->add_sql_clause( 'join', "JOIN {$wpdb->prefix}woocommerce_tax_rates ON default_results.tax_rate_id = {$wpdb->prefix}woocommerce_tax_rates.tax_rate_id" );
		} else {
			$this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->prefix}woocommerce_tax_rates ON {$table_name}.tax_rate_id = {$wpdb->prefix}woocommerce_tax_rates.tax_rate_id" );
		}
	}

	/**
	 * Updates the database query with parameters used for Taxes report: categories and order status.
	 *
	 * @param array $query_args Query arguments supplied by the user.
	 */
	protected function add_sql_query_params( $query_args ) {
		global $wpdb;

		$order_tax_lookup_table = self::get_db_table_name();

		$this->add_time_period_sql_params( $query_args, $order_tax_lookup_table );
		$this->get_limit_sql_params( $query_args );
		$this->add_order_by_sql_params( $query_args );
		$order_status_filter = $this->get_status_subquery( $query_args );
		$this->add_from_sql_params( $query_args, $order_status_filter );

		if ( isset( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
			$allowed_taxes = self::get_filtered_ids( $query_args, 'taxes' );
			$this->subquery->add_sql_clause( 'where', "AND {$order_tax_lookup_table}.tax_rate_id IN ({$allowed_taxes})" );
		}

		if ( $order_status_filter ) {
			$this->subquery->add_sql_clause( 'where', "AND ( {$order_status_filter} )" );
		}
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page' => get_option( 'posts_per_page' ),
			'page'     => 1,
			'order'    => 'DESC',
			'orderby'  => 'tax_rate_id',
			'before'   => TimeInterval::default_before(),
			'after'    => TimeInterval::default_after(),
			'fields'   => '*',
			'taxes'    => array(),
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$data = (object) array(
				'data'    => array(),
				'total'   => 0,
				'pages'   => 0,
				'page_no' => 0,
			);

			$this->add_sql_query_params( $query_args );
			$params = $this->get_limit_params( $query_args );

			if ( isset( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
				$total_results = count( $query_args['taxes'] );
				$total_pages   = (int) ceil( $total_results / $params['per_page'] );

				$inner_selections = array( 'tax_rate_id', 'total_tax', 'order_tax', 'shipping_tax', 'orders_count' );
				$outer_selections = array( 'name', 'tax_rate', 'country', 'state', 'priority' );

				$selections      = $this->selected_columns( array( 'fields' => $inner_selections ) );
				$fields          = $this->get_fields( $query_args );
				$join_selections = $this->format_join_selections( $fields, array( 'tax_rate_id' ), $outer_selections );
				$ids_table       = $this->get_ids_table( $query_args['taxes'], 'tax_rate_id' );

				$this->subquery->clear_sql_clause( 'select' );
				$this->subquery->add_sql_clause( 'select', $this->selected_columns( array( 'fields' => $inner_selections ) ) );
				$this->add_sql_clause( 'select', $join_selections );
				$this->add_sql_clause( 'from', '(' );
				$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
				$this->add_sql_clause( 'from', ") AS {$table_name}" );
				$this->add_sql_clause(
					'right_join',
					"RIGHT JOIN ( {$ids_table} ) AS default_results
					ON default_results.tax_rate_id = {$table_name}.tax_rate_id"
				);

				$taxes_query = $this->get_query_statement();
			} else {
				$db_records_count = (int) $wpdb->get_var(
					"SELECT COUNT(*) FROM (
						{$this->subquery->get_query_statement()}
					) AS tt"
				); // WPCS: cache ok, DB call ok, unprepared SQL ok.

				$total_results = $db_records_count;
				$total_pages   = (int) ceil( $db_records_count / $params['per_page'] );

				if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
					return $data;
				}

				$this->subquery->clear_sql_clause( 'select' );
				$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
				$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
				$taxes_query = $this->subquery->get_query_statement();
			}

			$tax_data = $wpdb->get_results(
				$taxes_query,
				ARRAY_A
			); // WPCS: cache ok, DB call ok, unprepared SQL ok.

			if ( null === $tax_data ) {
				return $data;
			}

			$tax_data = array_map( array( $this, 'cast_numbers' ), $tax_data );
			$data     = (object) array(
				'data'    => $tax_data,
				'total'   => $total_results,
				'pages'   => $total_pages,
				'page_no' => (int) $query_args['page'],
			);

			$this->set_cached_data( $cache_key, $data );
		}

		return $data;
	}

	/**
	 * Maps ordering specified by the user to columns in the database/fields in the data.
	 *
	 * @param string $order_by Sorting criterion.
	 * @return string
	 */
	protected function normalize_order_by( $order_by ) {
		global $wpdb;

		if ( 'tax_code' === $order_by ) {
			return 'CONCAT_WS( "-", NULLIF(tax_rate_country, ""), NULLIF(tax_rate_state, ""), NULLIF(tax_rate_name, ""), NULLIF(tax_rate_priority, "") )';
		} elseif ( 'rate' === $order_by ) {
			return "CAST({$wpdb->prefix}woocommerce_tax_rates.tax_rate as DECIMAL(7,4))";
		}

		return $order_by;
	}

	/**
	 * Create or update an entry in the wc_order_tax_lookup table for an order.
	 *
	 * @param int $order_id Order ID.
	 * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
	 */
	public static function sync_order_taxes( $order_id ) {
		global $wpdb;

		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return -1;
		}

		$tax_items   = $order->get_items( 'tax' );
		$num_updated = 0;

		foreach ( $tax_items as $tax_item ) {
			$result = $wpdb->replace(
				self::get_db_table_name(),
				array(
					'order_id'     => $order->get_id(),
					'date_created' => $order->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ),
					'tax_rate_id'  => $tax_item->get_rate_id(),
					'shipping_tax' => $tax_item->get_shipping_tax_total(),
					'order_tax'    => $tax_item->get_tax_total(),
					'total_tax'    => (float) $tax_item->get_tax_total() + (float) $tax_item->get_shipping_tax_total(),
				),
				array(
					'%d',
					'%s',
					'%d',
					'%f',
					'%f',
					'%f',
				)
			);

			/**
			 * Fires when tax's reports are updated.
			 *
			 * @param int $tax_rate_id Tax Rate ID.
			 * @param int $order_id    Order ID.
			 */
			do_action( 'woocommerce_analytics_update_tax', $tax_item->get_rate_id(), $order->get_id() );

			// Sum the rows affected. Using REPLACE can affect 2 rows if the row already exists.
			$num_updated += 2 === intval( $result ) ? 1 : intval( $result );
		}

		return ( count( $tax_items ) === $num_updated );
	}

	/**
	 * Clean taxes data when an order is deleted.
	 *
	 * @param int $order_id Order ID.
	 */
	public static function sync_on_order_delete( $order_id ) {
		global $wpdb;

		$wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) );

		/**
		 * Fires when tax's reports are removed from database.
		 *
		 * @param int $tax_rate_id Tax Rate ID.
		 * @param int $order_id    Order ID.
		 */
		do_action( 'woocommerce_analytics_delete_tax', 0, $order_id );

		ReportsCache::invalidate();
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		$this->subquery = new SqlQuery( $this->context . '_subquery' );
		$this->subquery->add_sql_clause( 'select', self::get_db_table_name() . '.tax_rate_id' );
		$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
		$this->subquery->add_sql_clause( 'group_by', self::get_db_table_name() . '.tax_rate_id' );
	}
}
Admin/API/Reports/Taxes/Query.php000064400000002246152333011040012541 0ustar00<?php
/**
 * Class for parameter-based Taxes Report querying
 *
 * Example usage:
 * $args = array(
 *          'before'       => '2018-07-19 00:00:00',
 *          'after'        => '2018-07-05 00:00:00',
 *          'page'         => 2,
 *          'taxes'        => array(1,2,3)
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Taxes\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Taxes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Taxes\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Taxes report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array();
	}

	/**
	 * Get product data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_taxes_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-taxes' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_taxes_select_query', $results, $args );
	}
}
Admin/API/Reports/Taxes/Stats/Controller.php000064400000031750152333011040014657 0ustar00<?php
/**
 * REST API Reports taxes stats controller
 *
 * Handles requests to the /reports/taxes/stats endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats;

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports taxes stats controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/taxes/stats';

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_filter( 'woocommerce_analytics_taxes_stats_select_query', array( $this, 'set_default_report_data' ) );
	}

	/**
	 * Set the default results to 0 if API returns an empty array
	 *
	 * @internal
	 * @param Mixed $results Report data.
	 * @return object
	 */
	public function set_default_report_data( $results ) {
		if ( empty( $results ) ) {
			$results                       = new \stdClass();
			$results->total                = 0;
			$results->totals               = new \stdClass();
			$results->totals->tax_codes    = 0;
			$results->totals->total_tax    = 0;
			$results->totals->order_tax    = 0;
			$results->totals->shipping_tax = 0;
			$results->totals->orders       = 0;
			$results->intervals            = array();
			$results->pages                = 1;
			$results->page_no              = 1;
		}
		return $results;
	}

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args                        = array();
		$args['before']              = $request['before'];
		$args['after']               = $request['after'];
		$args['interval']            = $request['interval'];
		$args['page']                = $request['page'];
		$args['per_page']            = $request['per_page'];
		$args['orderby']             = $request['orderby'];
		$args['order']               = $request['order'];
		$args['taxes']               = (array) $request['taxes'];
		$args['segmentby']           = $request['segmentby'];
		$args['fields']              = $request['fields'];
		$args['force_cache_refresh'] = $request['force_cache_refresh'];

		return $args;
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$query_args  = $this->prepare_reports_query( $request );
		$taxes_query = new Query( $query_args );
		$report_data = $taxes_query->get_data();

		$out_data = array(
			'totals'    => get_object_vars( $report_data->totals ),
			'intervals' => array(),
		);

		foreach ( $report_data->intervals as $interval_data ) {
			$item                    = $this->prepare_item_for_response( (object) $interval_data, $request );
			$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $out_data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		$page      = $report_data->page_no;
		$max_pages = $report_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param stdClass        $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = get_object_vars( $report );

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_taxes_stats', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$data_values = array(
			'total_tax'    => array(
				'description' => __( 'Total tax.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
				'format'      => 'currency',
			),
			'order_tax'    => array(
				'description' => __( 'Order tax.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
				'format'      => 'currency',
			),
			'shipping_tax' => array(
				'description' => __( 'Shipping tax.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
				'format'      => 'currency',
			),
			'orders_count' => array(
				'description' => __( 'Number of orders.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'tax_codes'    => array(
				'description' => __( 'Amount of tax codes.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
		);

		$segments = array(
			'segments' => array(
				'description' => __( 'Reports data grouped by segment condition.', 'woocommerce' ),
				'type'        => 'array',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'items'       => array(
					'type'       => 'object',
					'properties' => array(
						'segment_id' => array(
							'description' => __( 'Segment identificator.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'subtotals'  => array(
							'description' => __( 'Interval subtotals.', 'woocommerce' ),
							'type'        => 'object',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
							'properties'  => $data_values,
						),
					),
				),
			),
		);

		$totals = array_merge( $data_values, $segments );

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_taxes_stats',
			'type'       => 'object',
			'properties' => array(
				'totals'    => array(
					'description' => __( 'Totals data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'properties'  => $totals,
				),
				'intervals' => array(
					'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'interval'       => array(
								'description' => __( 'Type of interval.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'enum'        => array( 'day', 'week', 'month', 'year' ),
							),
							'date_start'     => array(
								'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_start_gmt' => array(
								'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end'       => array(
								'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end_gmt'   => array(
								'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'subtotals'      => array(
								'description' => __( 'Interval subtotals.', 'woocommerce' ),
								'type'        => 'object',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'properties'  => $totals,
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params              = array();
		$params['context']   = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']      = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']  = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']     = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']    = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']     = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']   = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'date',
				'items_sold',
				'total_sales',
				'orders_count',
				'products_count',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['interval']  = array(
			'description'       => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'week',
			'enum'              => array(
				'hour',
				'day',
				'week',
				'month',
				'quarter',
				'year',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['taxes']     = array(
			'description'       => __( 'Limit result set to all items that have the specified term assigned in the taxes taxonomy.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['segmentby'] = array(
			'description'       => __( 'Segment the response by additional constraint.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array(
				'tax_rate_id',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['fields']    = array(
			'description'       => __( 'Limit stats fields to the specified items.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
Admin/API/Reports/Taxes/Stats/Query.php000064400000002377152333011040013644 0ustar00<?php
/**
 * Class for parameter-based Taxes Stats Report querying
 *
 * Example usage:
 * $args = array(
 *          'before'       => '2018-07-19 00:00:00',
 *          'after'        => '2018-07-05 00:00:00',
 *          'page'         => 2,
 *          'categories'   => array(15, 18),
 *          'product_ids'  => array(1,2,3)
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Taxes\Stats\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Taxes report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array();
	}

	/**
	 * Get tax stats data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_taxes_stats_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-taxes-stats' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_taxes_stats_select_query', $results, $args );
	}
}
Admin/API/Reports/Taxes/Stats/DataStore.php000064400000023642152333011040014423 0ustar00<?php
/**
 * API\Reports\Taxes\Stats\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;

/**
 * API\Reports\Taxes\Stats\DataStore.
 */
class DataStore extends ReportsDataStore implements DataStoreInterface {

	/**
	 * Table used to get the data.
	 *
	 * @var string
	 */
	protected static $table_name = 'wc_order_tax_lookup';

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'taxes_stats';

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'tax_codes'    => 'intval',
		'total_tax'    => 'floatval',
		'order_tax'    => 'floatval',
		'shipping_tax' => 'floatval',
		'orders_count' => 'intval',
	);

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'taxes_stats';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$table_name           = self::get_db_table_name();
		$this->report_columns = array(
			'tax_codes'    => 'COUNT(DISTINCT tax_rate_id) as tax_codes',
			'total_tax'    => 'SUM(total_tax) AS total_tax',
			'order_tax'    => 'SUM(order_tax) as order_tax',
			'shipping_tax' => 'SUM(shipping_tax) as shipping_tax',
			'orders_count' => "COUNT( DISTINCT ( CASE WHEN parent_id = 0 THEN {$table_name}.order_id END ) ) as orders_count",
		);
	}

	/**
	 * Updates the database query with parameters used for Taxes Stats report
	 *
	 * @param array $query_args       Query arguments supplied by the user.
	 */
	protected function update_sql_query_params( $query_args ) {
		global $wpdb;

		$taxes_where_clause     = '';
		$order_tax_lookup_table = self::get_db_table_name();

		if ( isset( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
			$tax_id_placeholders = implode( ',', array_fill( 0, count( $query_args['taxes'] ), '%d' ) );
			/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
			$taxes_where_clause .= $wpdb->prepare( " AND {$order_tax_lookup_table}.tax_rate_id IN ({$tax_id_placeholders})", $query_args['taxes'] );
			/* phpcs:enable */
		}

		$order_status_filter = $this->get_status_subquery( $query_args );
		if ( $order_status_filter ) {
			$taxes_where_clause .= " AND ( {$order_status_filter} )";
		}

		$this->add_time_period_sql_params( $query_args, $order_tax_lookup_table );
		$this->total_query->add_sql_clause( 'where', $taxes_where_clause );

		$this->add_intervals_sql_params( $query_args, $order_tax_lookup_table );
		$this->interval_query->add_sql_clause( 'where', $taxes_where_clause );
		$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
		$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
	}

	/**
	 * Get taxes associated with a store.
	 *
	 * @param array $args Array of args to filter the query by. Supports `include`.
	 * @return array An array of all taxes.
	 */
	public static function get_taxes( $args ) {
		global $wpdb;
		$query = "
			SELECT 
				tax_rate_id, 
				tax_rate_country, 
				tax_rate_state, 
				tax_rate_name, 
				tax_rate_priority 
			FROM {$wpdb->prefix}woocommerce_tax_rates
		";
		if ( ! empty( $args['include'] ) ) {
			/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
			$tax_placeholders = implode( ',', array_fill( 0, count( $args['include'] ), '%d' ) );
			$query           .= $wpdb->prepare( " WHERE tax_rate_id IN ({$tax_placeholders})", $args['include'] );
			/* phpcs:enable */
		}
		return $wpdb->get_results( $query, ARRAY_A ); // WPCS: cache ok, DB call ok, unprepared SQL ok.
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page' => get_option( 'posts_per_page' ),
			'page'     => 1,
			'order'    => 'DESC',
			'orderby'  => 'tax_rate_id',
			'before'   => TimeInterval::default_before(),
			'after'    => TimeInterval::default_after(),
			'fields'   => '*',
			'taxes'    => array(),
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$data = (object) array(
				'totals'    => (object) array(),
				'intervals' => (object) array(),
				'total'     => 0,
				'pages'     => 0,
				'page_no'   => 0,
			);

			$selections       = $this->selected_columns( $query_args );
			$params           = $this->get_limit_params( $query_args );
			$order_stats_join = "JOIN {$wpdb->prefix}wc_order_stats ON {$table_name}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
			$this->update_sql_query_params( $query_args );
			$this->interval_query->add_sql_clause( 'join', $order_stats_join );

			$db_intervals            = $wpdb->get_col(
				$this->interval_query->get_query_statement()
			); // WPCS: cache ok, DB call ok, unprepared SQL ok.
			$db_interval_count       = count( $db_intervals );
			$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
			$total_pages             = (int) ceil( $expected_interval_count / $params['per_page'] );

			if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
				return $data;
			}
			$this->total_query->add_sql_clause( 'select', $selections );
			$this->total_query->add_sql_clause( 'join', $order_stats_join );
			$this->total_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );

			$totals = $wpdb->get_results(
				$this->total_query->get_query_statement(),
				ARRAY_A
			); // WPCS: cache ok, DB call ok, unprepared SQL ok.

			if ( null === $totals ) {
				return new \WP_Error( 'woocommerce_analytics_taxes_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
			}

			// @todo remove these assignements when refactoring segmenter classes to use query objects.
			$totals_query          = array(
				'from_clause'       => $this->total_query->get_sql_clause( 'join' ),
				'where_time_clause' => $this->total_query->get_sql_clause( 'where_time' ),
				'where_clause'      => $this->total_query->get_sql_clause( 'where' ),
			);
			$intervals_query       = array(
				'select_clause'     => $this->get_sql_clause( 'select' ),
				'from_clause'       => $this->interval_query->get_sql_clause( 'join' ),
				'where_time_clause' => $this->interval_query->get_sql_clause( 'where_time' ),
				'where_clause'      => $this->interval_query->get_sql_clause( 'where' ),
			);
			$segmenter             = new Segmenter( $query_args, $this->report_columns );
			$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );

			$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );

			if ( '' !== $selections ) {
				$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
			}

			$this->interval_query->add_sql_clause( 'select', ", MAX({$table_name}.date_created) AS datetime_anchor" );
			$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
			$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );

			$intervals = $wpdb->get_results(
				$this->interval_query->get_query_statement(),
				ARRAY_A
			); // WPCS: cache ok, DB call ok, unprepared SQL ok.

			if ( null === $intervals ) {
				return new \WP_Error( 'woocommerce_analytics_taxes_stats_result_failed', __( 'Sorry, fetching tax data failed.', 'woocommerce' ) );
			}

			$totals = (object) $this->cast_numbers( $totals[0] );

			$data = (object) array(
				'totals'    => $totals,
				'intervals' => $intervals,
				'total'     => $expected_interval_count,
				'pages'     => $total_pages,
				'page_no'   => (int) $query_args['page'],
			);

			if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
				$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
				$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
				$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
			} else {
				$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
			}
			$segmenter->add_intervals_segments( $data, $intervals_query, $table_name );
			$this->create_interval_subtotals( $data->intervals );
			$this->set_cached_data( $cache_key, $data );
		}
		return $data;
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		unset( $this->subquery );
		$this->total_query = new SqlQuery( $this->context . '_total' );
		$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );

		$this->interval_query = new SqlQuery( $this->context . '_interval' );
		$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
		$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
	}
}
Admin/API/Reports/Taxes/Stats/Segmenter.php000064400000013032152333011040014456 0ustar00<?php
/**
 * Class for adding segmenting support without cluttering the data stores.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Segmenter as ReportsSegmenter;

/**
 * Date & time interval and numeric range handling class for Reporting API.
 */
class Segmenter extends ReportsSegmenter {

	/**
	 * Returns column => query mapping to be used for order-related order-level segmenting query (e.g. tax_rate_id).
	 *
	 * @param string $lookup_table Name of SQL table containing the order-level segmenting info.
	 *
	 * @return array Column => SELECT query mapping.
	 */
	protected function get_segment_selections_order_level( $lookup_table ) {
		$columns_mapping = array(
			'tax_codes'    => "COUNT(DISTINCT $lookup_table.tax_rate_id) as tax_codes",
			'total_tax'    => "SUM($lookup_table.total_tax) AS total_tax",
			'order_tax'    => "SUM($lookup_table.order_tax) as order_tax",
			'shipping_tax' => "SUM($lookup_table.shipping_tax) as shipping_tax",
			'orders_count' => "COUNT(DISTINCT $lookup_table.order_id) as orders_count",
		);

		return $columns_mapping;
	}

	/**
	 * Calculate segments for totals query where the segmenting property is bound to order (e.g. coupon or customer type).
	 *
	 * @param string $segmenting_select SELECT part of segmenting SQL query.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $totals_query Array of SQL clauses for intervals query.
	 *
	 * @return array
	 */
	protected function get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $totals_query ) {
		global $wpdb;

		$totals_segments = $wpdb->get_results(
			"SELECT
						$segmenting_groupby
						$segmenting_select
					FROM
						$table_name
						$segmenting_from
						{$totals_query['from_clause']}
					WHERE
						1=1
						{$totals_query['where_time_clause']}
						{$totals_query['where_clause']}
						$segmenting_where
					GROUP BY
						$segmenting_groupby",
			ARRAY_A
		); // WPCS: cache ok, DB call ok, unprepared SQL ok.

		// Reformat result.
		$totals_segments = $this->reformat_totals_segments( $totals_segments, $segmenting_groupby );
		return $totals_segments;
	}

	/**
	 * Calculate segments for intervals query where the segmenting property is bound to order (e.g. coupon or customer type).
	 *
	 * @param string $segmenting_select SELECT part of segmenting SQL query.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $intervals_query Array of SQL clauses for intervals query.
	 *
	 * @return array
	 */
	protected function get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $intervals_query ) {
		global $wpdb;
		$segmenting_limit = '';
		$limit_parts      = explode( ',', $intervals_query['limit'] );
		if ( 2 === count( $limit_parts ) ) {
			$orig_rowcount    = intval( $limit_parts[1] );
			$segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() );
		}

		$intervals_segments = $wpdb->get_results(
			"SELECT
						MAX($table_name.date_created) AS datetime_anchor,
						{$intervals_query['select_clause']} AS time_interval,
						$segmenting_groupby
						$segmenting_select
					FROM
						$table_name
						$segmenting_from
						{$intervals_query['from_clause']}
					WHERE
						1=1
						{$intervals_query['where_time_clause']}
						{$intervals_query['where_clause']}
						$segmenting_where
					GROUP BY
						time_interval, $segmenting_groupby
					$segmenting_limit",
			ARRAY_A
		); // WPCS: cache ok, DB call ok, unprepared SQL ok.

		// Reformat result.
		$intervals_segments = $this->reformat_intervals_segments( $intervals_segments, $segmenting_groupby );
		return $intervals_segments;
	}

	/**
	 * Return array of segments formatted for REST response.
	 *
	 * @param string $type Type of segments to return--'totals' or 'intervals'.
	 * @param array  $query_params SQL query parameter array.
	 * @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
	 *
	 * @return array
	 * @throws \Automattic\WooCommerce\Admin\API\Reports\ParameterException In case of segmenting by variations, when no parent product is specified.
	 */
	protected function get_segments( $type, $query_params, $table_name ) {
		if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
			return array();
		}

		$segmenting_where = '';
		$segmenting_from  = '';
		$segments         = array();

		if ( 'tax_rate_id' === $this->query_args['segmentby'] ) {
			$tax_rate_level_columns = $this->get_segment_selections_order_level( $table_name );
			$segmenting_select      = $this->prepare_selections( $tax_rate_level_columns );
			$this->report_columns   = $tax_rate_level_columns;
			$segmenting_groupby     = $table_name . '.tax_rate_id';

			$segments = $this->get_order_related_segments( $type, $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
		}

		return $segments;
	}
}
Admin/API/Reports/Taxes/Controller.php000064400000024420152333011040013555 0ustar00<?php
/**
 * REST API Reports taxes controller
 *
 * Handles requests to the /reports/taxes endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Taxes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;

/**
 * REST API Reports taxes controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
	/**
	 * Exportable traits.
	 */
	use ExportableTraits;

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/taxes';

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args                        = array();
		$args['before']              = $request['before'];
		$args['after']               = $request['after'];
		$args['page']                = $request['page'];
		$args['per_page']            = $request['per_page'];
		$args['orderby']             = $request['orderby'];
		$args['order']               = $request['order'];
		$args['taxes']               = $request['taxes'];
		$args['force_cache_refresh'] = $request['force_cache_refresh'];

		return $args;
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$query_args  = $this->prepare_reports_query( $request );
		$taxes_query = new Query( $query_args );
		$report_data = $taxes_query->get_data();

		$data = array();

		foreach ( $report_data->data as $tax_data ) {
			$item   = $this->prepare_item_for_response( (object) $tax_data, $request );
			$data[] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		$page      = $report_data->page_no;
		$max_pages = $report_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param stdClass        $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$report  = $this->add_additional_fields_to_object( $report, $request );
		$report  = $this->filter_response_by_context( $report, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $report );
		$response->add_links( $this->prepare_links( $report ) );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_taxes', $response, $report, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WC_Reports_Query $object Object data.
	 * @return array
	 */
	protected function prepare_links( $object ) {
		$links = array(
			'tax' => array(
				'href' => rest_url( sprintf( '/%s/taxes/%d', $this->namespace, $object->tax_rate_id ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_taxes',
			'type'       => 'object',
			'properties' => array(
				'tax_rate_id'  => array(
					'description' => __( 'Tax rate ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'         => array(
					'description' => __( 'Tax rate name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'tax_rate'     => array(
					'description' => __( 'Tax rate.', 'woocommerce' ),
					'type'        => 'number',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'country'      => array(
					'description' => __( 'Country / Region.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'state'        => array(
					'description' => __( 'State.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'priority'     => array(
					'description' => __( 'Priority.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total_tax'    => array(
					'description' => __( 'Total tax.', 'woocommerce' ),
					'type'        => 'number',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'order_tax'    => array(
					'description' => __( 'Order tax.', 'woocommerce' ),
					'type'        => 'number',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_tax' => array(
					'description' => __( 'Shipping tax.', 'woocommerce' ),
					'type'        => 'number',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'orders_count' => array(
					'description' => __( 'Number of orders.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params             = array();
		$params['context']  = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']     = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page'] = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']    = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']   = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']    = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']  = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'tax_rate_id',
			'enum'              => array(
				'name',
				'tax_rate_id',
				'tax_code',
				'rate',
				'order_tax',
				'total_tax',
				'shipping_tax',
				'orders_count',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['taxes']    = array(
			'description'       => __( 'Limit result set to items assigned one or more tax rates.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get the column names for export.
	 *
	 * @return array Key value pair of Column ID => Label.
	 */
	public function get_export_columns() {
		return array(
			'tax_code'     => __( 'Tax code', 'woocommerce' ),
			'rate'         => __( 'Rate', 'woocommerce' ),
			'total_tax'    => __( 'Total tax', 'woocommerce' ),
			'order_tax'    => __( 'Order tax', 'woocommerce' ),
			'shipping_tax' => __( 'Shipping tax', 'woocommerce' ),
			'orders_count' => __( 'Orders', 'woocommerce' ),
		);
	}

	/**
	 * Get the column values for export.
	 *
	 * @param array $item Single report item/row.
	 * @return array Key value pair of Column ID => Row Value.
	 */
	public function prepare_item_for_export( $item ) {
		return array(
			'tax_code'     => \WC_Tax::get_rate_code( $item['tax_rate_id'] ),
			'rate'         => $item['tax_rate'],
			'total_tax'    => self::csv_number_format( $item['total_tax'] ),
			'order_tax'    => self::csv_number_format( $item['order_tax'] ),
			'shipping_tax' => self::csv_number_format( $item['shipping_tax'] ),
			'orders_count' => $item['orders_count'],
		);
	}
}
Admin/API/Reports/Controller.php000064400000024217152333011040012475 0ustar00<?php
/**
 * REST API Reports controller extended by WC Admin plugin.
 *
 * Handles requests to the reports endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports;

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports';

	/**
	 * Register the routes for reports.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to read reports.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}


	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$data    = array();
		$reports = array(
			array(
				'slug'        => 'performance-indicators',
				'description' => __( 'Batch endpoint for getting specific performance indicators from `stats` endpoints.', 'woocommerce' ),
			),
			array(
				'slug'        => 'revenue/stats',
				'description' => __( 'Stats about revenue.', 'woocommerce' ),
			),
			array(
				'slug'        => 'orders/stats',
				'description' => __( 'Stats about orders.', 'woocommerce' ),
			),
			array(
				'slug'        => 'products',
				'description' => __( 'Products detailed reports.', 'woocommerce' ),
			),
			array(
				'slug'        => 'products/stats',
				'description' => __( 'Stats about products.', 'woocommerce' ),
			),
			array(
				'slug'        => 'variations',
				'description' => __( 'Variations detailed reports.', 'woocommerce' ),
			),
			array(
				'slug'        => 'variations/stats',
				'description' => __( 'Stats about variations.', 'woocommerce' ),
			),
			array(
				'slug'        => 'categories',
				'description' => __( 'Product categories detailed reports.', 'woocommerce' ),
			),
			array(
				'slug'        => 'categories/stats',
				'description' => __( 'Stats about product categories.', 'woocommerce' ),
			),
			array(
				'slug'        => 'coupons',
				'description' => __( 'Coupons detailed reports.', 'woocommerce' ),
			),
			array(
				'slug'        => 'coupons/stats',
				'description' => __( 'Stats about coupons.', 'woocommerce' ),
			),
			array(
				'slug'        => 'taxes',
				'description' => __( 'Taxes detailed reports.', 'woocommerce' ),
			),
			array(
				'slug'        => 'taxes/stats',
				'description' => __( 'Stats about taxes.', 'woocommerce' ),
			),
			array(
				'slug'        => 'downloads',
				'description' => __( 'Product downloads detailed reports.', 'woocommerce' ),
			),
			array(
				'slug'        => 'downloads/files',
				'description' => __( 'Product download files detailed reports.', 'woocommerce' ),
			),
			array(
				'slug'        => 'downloads/stats',
				'description' => __( 'Stats about product downloads.', 'woocommerce' ),
			),
			array(
				'slug'        => 'customers',
				'description' => __( 'Customers detailed reports.', 'woocommerce' ),
			),
		);

		/**
		 * Filter the list of allowed reports, so that data can be loaded from third party extensions in addition to WooCommerce core.
		 * Array items should be in format of array( 'slug' => 'downloads/stats', 'description' =>  '',
		 * 'url' => '', and 'path' => '/wc-ext/v1/...'.
		 *
		 * @param array $endpoints The list of allowed reports..
		 */
		$reports = apply_filters( 'woocommerce_admin_reports', $reports );

		foreach ( $reports as $report ) {
			if ( empty( $report['slug'] ) ) {
				continue;
			}

			if ( empty( $report['path'] ) ) {
				$report['path'] = '/' . $this->namespace . '/reports/' . $report['slug'];
			}

			// Allows a different admin page to be loaded here,
			// or allows an empty url if no report exists for a set of performance indicators.
			if ( ! isset( $report['url'] ) ) {
				if ( '/stats' === substr( $report['slug'], -6 ) ) {
					$url_slug = substr( $report['slug'], 0, -6 );
				} else {
					$url_slug = $report['slug'];
				}

				$report['url'] = '/analytics/' . $url_slug;
			}

			$item   = $this->prepare_item_for_response( (object) $report, $request );
			$data[] = $this->prepare_response_for_collection( $item );
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Get the order number for an order. If no filter is present for `woocommerce_order_number`, we can just return the ID.
	 * Returns the parent order number if the order is actually a refund.
	 *
	 * @param  int $order_id Order ID.
	 * @return string
	 */
	protected function get_order_number( $order_id ) {
		$order = wc_get_order( $order_id );

		if ( ! $order instanceof \WC_Order && ! $order instanceof \WC_Order_Refund ) {
			return null;
		}

		if ( 'shop_order_refund' === $order->get_type() ) {
			$order = wc_get_order( $order->get_parent_id() );
		}

		if ( ! has_filter( 'woocommerce_order_number' ) ) {
			return $order->get_id();
		}

		return $order->get_order_number();
	}

	/**
	 * Get the order total with the related currency formatting.
	 * Returns the parent order total if the order is actually a refund.
	 *
	 * @param  int $order_id Order ID.
	 * @return string
	 */
	protected function get_total_formatted( $order_id ) {
		$order = wc_get_order( $order_id );

		if ( ! $order instanceof \WC_Order && ! $order instanceof \WC_Order_Refund ) {
			return null;
		}

		if ( 'shop_order_refund' === $order->get_type() ) {
			$order = wc_get_order( $order->get_parent_id() );
		}

		return wp_strip_all_tags( html_entity_decode( $order->get_formatted_order_total() ), true );
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param stdClass        $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = array(
			'slug'        => $report->slug,
			'description' => $report->description,
			'path'        => $report->path,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links(
			array(
				'self'       => array(
					'href' => rest_url( $report->path ),
				),
				'report'     => array(
					'href' => $report->url,
				),
				'collection' => array(
					'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
				),
			)
		);

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report',
			'type'       => 'object',
			'properties' => array(
				'slug'        => array(
					'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'description' => array(
					'description' => __( 'A human-readable description of the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'path'        => array(
					'description' => __( 'API path.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}

	/**
	 * Get order statuses without prefixes.
	 * Includes unregistered statuses that have been marked "actionable".
	 *
	 * @internal
	 * @return array
	 */
	public static function get_order_statuses() {
		// Allow all statuses selected as "actionable" - this may include unregistered statuses.
		// See: https://github.com/woocommerce/woocommerce-admin/issues/5592.
		$actionable_statuses = get_option( 'woocommerce_actionable_order_statuses', array() );

		// See WC_REST_Orders_V2_Controller::get_collection_params() re: any/trash statuses.
		$registered_statuses = array_merge( array( 'any', 'trash' ), array_keys( self::get_order_status_labels() ) );

		// Merge the status arrays (using flip to avoid array_unique()).
		$allowed_statuses = array_keys( array_merge( array_flip( $registered_statuses ), array_flip( $actionable_statuses ) ) );

		return $allowed_statuses;
	}

	/**
	 * Get order statuses (and labels) without prefixes.
	 *
	 * @internal
	 * @return array
	 */
	public static function get_order_status_labels() {
		$order_statuses = array();

		foreach ( wc_get_order_statuses() as $key => $label ) {
			$new_key                    = str_replace( 'wc-', '', $key );
			$order_statuses[ $new_key ] = $label;
		}

		return $order_statuses;
	}
}
Admin/API/Reports/TimeInterval.php000064400000056655152333011040012770 0ustar00<?php
/**
 * Class for time interval and numeric range handling for reports.
 */

namespace Automattic\WooCommerce\Admin\API\Reports;

defined( 'ABSPATH' ) || exit;

/**
 * Date & time interval and numeric range handling class for Reporting API.
 */
class TimeInterval {

	/**
	 * Format string for ISO DateTime formatter.
	 *
	 * @var string
	 */
	public static $iso_datetime_format = 'Y-m-d\TH:i:s';

	/**
	 * Format string for use in SQL queries.
	 *
	 * @var string
	 */
	public static $sql_datetime_format = 'Y-m-d H:i:s';

	/**
	 * Converts local datetime to GMT/UTC time.
	 *
	 * @param string $datetime_string String representation of local datetime.
	 * @return DateTime
	 */
	public static function convert_local_datetime_to_gmt( $datetime_string ) {
		$datetime = new \DateTime( $datetime_string, new \DateTimeZone( wc_timezone_string() ) );
		$datetime->setTimezone( new \DateTimeZone( 'GMT' ) );
		return $datetime;
	}

	/**
	 * Returns default 'before' parameter for the reports.
	 *
	 * @return DateTime
	 */
	public static function default_before() {
		$datetime = new \WC_DateTime();
		// Set local timezone or offset.
		if ( get_option( 'timezone_string' ) ) {
			$datetime->setTimezone( new \DateTimeZone( wc_timezone_string() ) );
		} else {
			$datetime->set_utc_offset( wc_timezone_offset() );
		}
		return $datetime;
	}

	/**
	 * Returns default 'after' parameter for the reports.
	 *
	 * @return DateTime
	 */
	public static function default_after() {
		$now       = time();
		$week_back = $now - WEEK_IN_SECONDS;

		$datetime = new \WC_DateTime();
		$datetime->setTimestamp( $week_back );
		// Set local timezone or offset.
		if ( get_option( 'timezone_string' ) ) {
			$datetime->setTimezone( new \DateTimeZone( wc_timezone_string() ) );
		} else {
			$datetime->set_utc_offset( wc_timezone_offset() );
		}
		return $datetime;
	}

	/**
	 * Returns date format to be used as grouping clause in SQL.
	 *
	 * @param string $time_interval Time interval.
	 * @param string $table_name Name of the db table relevant for the date constraint.
	 * @param string $date_column_name Name of the date table column.
	 * @return mixed
	 */
	public static function db_datetime_format( $time_interval, $table_name, $date_column_name = 'date_created' ) {
		$first_day_of_week = absint( get_option( 'start_of_week' ) );

		if ( 1 === $first_day_of_week ) {
			// Week begins on Monday, ISO 8601.
			$week_format = "DATE_FORMAT({$table_name}.`{$date_column_name}`, '%x-%v')";
		} else {
			// Week begins on day other than specified by ISO 8601, needs to be in sync with function simple_week_number.
			$week_format = "CONCAT(YEAR({$table_name}.`{$date_column_name}`), '-', LPAD( FLOOR( ( DAYOFYEAR({$table_name}.`{$date_column_name}`) + ( ( DATE_FORMAT(MAKEDATE(YEAR({$table_name}.`{$date_column_name}`),1), '%w') - $first_day_of_week + 7 ) % 7 ) - 1 ) / 7  ) + 1 , 2, '0'))";

		}

		// Whenever this is changed, double check method time_interval_id to make sure they are in sync.
		$mysql_date_format_mapping = array(
			'hour'    => "DATE_FORMAT({$table_name}.`{$date_column_name}`, '%Y-%m-%d %H')",
			'day'     => "DATE_FORMAT({$table_name}.`{$date_column_name}`, '%Y-%m-%d')",
			'week'    => $week_format,
			'month'   => "DATE_FORMAT({$table_name}.`{$date_column_name}`, '%Y-%m')",
			'quarter' => "CONCAT(YEAR({$table_name}.`{$date_column_name}`), '-', QUARTER({$table_name}.`{$date_column_name}`))",
			'year'    => "YEAR({$table_name}.`{$date_column_name}`)",

		);

		return $mysql_date_format_mapping[ $time_interval ];
	}

	/**
	 * Returns quarter for the DateTime.
	 *
	 * @param DateTime $datetime Local date & time.
	 * @return int|null
	 */
	public static function quarter( $datetime ) {
		switch ( (int) $datetime->format( 'm' ) ) {
			case 1:
			case 2:
			case 3:
				return 1;
			case 4:
			case 5:
			case 6:
				return 2;
			case 7:
			case 8:
			case 9:
				return 3;
			case 10:
			case 11:
			case 12:
				return 4;

		}
		return null;
	}

	/**
	 * Returns simple week number for the DateTime, for week starting on $first_day_of_week.
	 *
	 * The first week of the year is considered to be the week containing January 1.
	 * The second week starts on the next $first_day_of_week.
	 *
	 * @param DateTime $datetime          Local date for which the week number is to be calculated.
	 * @param int      $first_day_of_week 0 for Sunday to 6 for Saturday.
	 * @return int
	 */
	public static function simple_week_number( $datetime, $first_day_of_week ) {
		$beg_of_year_day          = new \DateTime( "{$datetime->format('Y')}-01-01" );
		$adj_day_beg_of_year      = ( (int) $beg_of_year_day->format( 'w' ) - $first_day_of_week + 7 ) % 7;
		$days_since_start_of_year = (int) $datetime->format( 'z' ) + 1;

		return (int) floor( ( ( $days_since_start_of_year + $adj_day_beg_of_year - 1 ) / 7 ) ) + 1;
	}

	/**
	 * Returns ISO 8601 week number for the DateTime, if week starts on Monday,
	 * otherwise returns simple week number.
	 *
	 * @see TimeInterval::simple_week_number()
	 *
	 * @param DateTime $datetime          Local date for which the week number is to be calculated.
	 * @param int      $first_day_of_week 0 for Sunday to 6 for Saturday.
	 * @return int
	 */
	public static function week_number( $datetime, $first_day_of_week ) {
		if ( 1 === $first_day_of_week ) {
			$week_number = (int) $datetime->format( 'W' );
		} else {
			$week_number = self::simple_week_number( $datetime, $first_day_of_week );
		}
		return $week_number;
	}

	/**
	 * Returns time interval id for the DateTime.
	 *
	 * @param string   $time_interval Time interval type (week, day, etc).
	 * @param DateTime $datetime      Date & time.
	 * @return string
	 */
	public static function time_interval_id( $time_interval, $datetime ) {
		// Whenever this is changed, double check method db_datetime_format to make sure they are in sync.
		$php_time_format_for = array(
			'hour'    => 'Y-m-d H',
			'day'     => 'Y-m-d',
			'week'    => 'o-W',
			'month'   => 'Y-m',
			'quarter' => 'Y-' . self::quarter( $datetime ),
			'year'    => 'Y',
		);

		// If the week does not begin on Monday.
		$first_day_of_week = absint( get_option( 'start_of_week' ) );

		if ( 'week' === $time_interval && 1 !== $first_day_of_week ) {
			$week_no = self::simple_week_number( $datetime, $first_day_of_week );
			$week_no = str_pad( $week_no, 2, '0', STR_PAD_LEFT );
			$year_no = $datetime->format( 'Y' );
			return "$year_no-$week_no";
		}

		return $datetime->format( $php_time_format_for[ $time_interval ] );
	}

	/**
	 * Calculates number of time intervals between two dates, closed interval on both sides.
	 *
	 * @param DateTime $start_datetime Start date & time.
	 * @param DateTime $end_datetime End date & time.
	 * @param string   $interval Time interval increment, e.g. hour, day, week.
	 *
	 * @return int
	 */
	public static function intervals_between( $start_datetime, $end_datetime, $interval ) {
		switch ( $interval ) {
			case 'hour':
				$end_timestamp   = (int) $end_datetime->format( 'U' );
				$start_timestamp = (int) $start_datetime->format( 'U' );
				$addendum        = 0;
				// modulo HOUR_IN_SECONDS would normally work, but there are non-full hour timezones, e.g. Nepal.
				$start_min_sec = (int) $start_datetime->format( 'i' ) * MINUTE_IN_SECONDS + (int) $start_datetime->format( 's' );
				$end_min_sec   = (int) $end_datetime->format( 'i' ) * MINUTE_IN_SECONDS + (int) $end_datetime->format( 's' );
				if ( $end_min_sec < $start_min_sec ) {
					$addendum = 1;
				}
				$diff_timestamp = $end_timestamp - $start_timestamp;

				return (int) floor( ( (int) $diff_timestamp ) / HOUR_IN_SECONDS ) + 1 + $addendum;
			case 'day':
				$days               = $start_datetime->diff( $end_datetime )->format( '%r%a' );
				$end_hour_min_sec   = (int) $end_datetime->format( 'H' ) * HOUR_IN_SECONDS + (int) $end_datetime->format( 'i' ) * MINUTE_IN_SECONDS + (int) $end_datetime->format( 's' );
				$start_hour_min_sec = (int) $start_datetime->format( 'H' ) * HOUR_IN_SECONDS + (int) $start_datetime->format( 'i' ) * MINUTE_IN_SECONDS + (int) $start_datetime->format( 's' );
				if ( $end_hour_min_sec < $start_hour_min_sec ) {
					$days++;
				}

				return $days + 1;
			case 'week':
				// @todo Optimize? approximately day count / 7, but year end is tricky, a week can have fewer days.
				$week_count = 0;
				do {
					$start_datetime = self::next_week_start( $start_datetime );
					$week_count++;
				} while ( $start_datetime <= $end_datetime );
				return $week_count;
			case 'month':
				// Year diff in months: (end_year - start_year - 1) * 12.
				$year_diff_in_months = ( (int) $end_datetime->format( 'Y' ) - (int) $start_datetime->format( 'Y' ) - 1 ) * 12;
				// All the months in end_date year plus months from X to 12 in the start_date year.
				$month_diff = (int) $end_datetime->format( 'n' ) + ( 12 - (int) $start_datetime->format( 'n' ) );
				// Add months for number of years between end_date and start_date.
				$month_diff += $year_diff_in_months + 1;
				return $month_diff;
			case 'quarter':
				// Year diff in quarters: (end_year - start_year - 1) * 4.
				$year_diff_in_quarters = ( (int) $end_datetime->format( 'Y' ) - (int) $start_datetime->format( 'Y' ) - 1 ) * 4;
				// All the quarters in end_date year plus quarters from X to 4 in the start_date year.
				$quarter_diff = self::quarter( $end_datetime ) + ( 4 - self::quarter( $start_datetime ) );
				// Add quarters for number of years between end_date and start_date.
				$quarter_diff += $year_diff_in_quarters + 1;
				return $quarter_diff;
			case 'year':
				$year_diff = (int) $end_datetime->format( 'Y' ) - (int) $start_datetime->format( 'Y' );
				return $year_diff + 1;
		}
		return 0;
	}

	/**
	 * Returns a new DateTime object representing the next hour start/previous hour end if reversed.
	 *
	 * @param DateTime $datetime Date and time.
	 * @param bool     $reversed Going backwards in time instead of forward.
	 * @return DateTime
	 */
	public static function next_hour_start( $datetime, $reversed = false ) {
		$hour_increment         = $reversed ? 0 : 1;
		$timestamp              = (int) $datetime->format( 'U' );
		$seconds_into_hour      = (int) $datetime->format( 'i' ) * MINUTE_IN_SECONDS + (int) $datetime->format( 's' );
		$hours_offset_timestamp = $timestamp + ( $hour_increment * HOUR_IN_SECONDS - $seconds_into_hour );

		if ( $reversed ) {
			$hours_offset_timestamp --;
		}

		$hours_offset_time = new \DateTime();
		$hours_offset_time->setTimestamp( $hours_offset_timestamp );
		$hours_offset_time->setTimezone( new \DateTimeZone( wc_timezone_string() ) );
		return $hours_offset_time;
	}

	/**
	 * Returns a new DateTime object representing the next day start, or previous day end if reversed.
	 *
	 * @param DateTime $datetime Date and time.
	 * @param bool     $reversed Going backwards in time instead of forward.
	 * @return DateTime
	 */
	public static function next_day_start( $datetime, $reversed = false ) {
		$oneday       = new \DateInterval( 'P1D' );
		$new_datetime = clone $datetime;

		if ( $reversed ) {
			$new_datetime->sub( $oneday );
			$new_datetime->setTime( 23, 59, 59 );
		} else {
			$new_datetime->add( $oneday );
			$new_datetime->setTime( 0, 0, 0 );
		}

		return $new_datetime;
	}

	/**
	 * Returns DateTime object representing the next week start, or previous week end if reversed.
	 *
	 * The next week start is the first day of the next week at 00:00:00.
	 * The previous week end is the last day of the previous week at 23:59:59.
	 * The start day is determined by the "start_of_week" wp_option.
	 *
	 * @param DateTime $datetime Date and time.
	 * @param bool     $reversed Going backwards in time instead of forward.
	 * @return DateTime
	 */
	public static function next_week_start( $datetime, $reversed = false ) {
		$seven_days = new \DateInterval( 'P7D' );
		// Default timezone set in wp-settings.php.
		$default_timezone = date_default_timezone_get();
		// Timezone that the WP site uses in Settings > General.
		$original_timezone = $datetime->getTimezone();
		// @codingStandardsIgnoreStart
		date_default_timezone_set( 'UTC' );
		$start_end_timestamp  = get_weekstartend( $datetime->format( 'Y-m-d' ) );
		date_default_timezone_set( $default_timezone );
		// @codingStandardsIgnoreEnd
		if ( $reversed ) {
			$result = \DateTime::createFromFormat( 'U', $start_end_timestamp['end'] )->sub( $seven_days );
		} else {
			$result = \DateTime::createFromFormat( 'U', $start_end_timestamp['start'] )->add( $seven_days );
		}
		return \DateTime::createFromFormat( 'Y-m-d H:i:s', $result->format( 'Y-m-d H:i:s' ), $original_timezone );
	}


	/**
	 * Returns a new DateTime object representing the next month start, or previous month end if reversed.
	 *
	 * @param DateTime $datetime Date and time.
	 * @param bool     $reversed Going backwards in time instead of forward.
	 * @return DateTime
	 */
	public static function next_month_start( $datetime, $reversed = false ) {
		$month_increment = 1;
		$year            = $datetime->format( 'Y' );
		$month           = (int) $datetime->format( 'm' );

		if ( $reversed ) {
			$beg_of_month_datetime       = new \DateTime( "$year-$month-01 00:00:00", new \DateTimeZone( wc_timezone_string() ) );
			$timestamp                   = (int) $beg_of_month_datetime->format( 'U' );
			$end_of_prev_month_timestamp = $timestamp - 1;
			$datetime->setTimestamp( $end_of_prev_month_timestamp );
		} else {
			$month += $month_increment;
			if ( $month > 12 ) {
				$month = 1;
				$year ++;
			}
			$day      = '01';
			$datetime = new \DateTime( "$year-$month-$day 00:00:00", new \DateTimeZone( wc_timezone_string() ) );
		}

		return $datetime;
	}

	/**
	 * Returns a new DateTime object representing the next quarter start, or previous quarter end if reversed.
	 *
	 * @param DateTime $datetime Date and time.
	 * @param bool     $reversed Going backwards in time instead of forward.
	 * @return DateTime
	 */
	public static function next_quarter_start( $datetime, $reversed = false ) {
		$year  = $datetime->format( 'Y' );
		$month = (int) $datetime->format( 'n' );

		switch ( $month ) {
			case 1:
			case 2:
			case 3:
				if ( $reversed ) {
					$month = 1;
				} else {
					$month = 4;
				}
				break;
			case 4:
			case 5:
			case 6:
				if ( $reversed ) {
					$month = 4;
				} else {
					$month = 7;
				}
				break;
			case 7:
			case 8:
			case 9:
				if ( $reversed ) {
					$month = 7;
				} else {
					$month = 10;
				}
				break;
			case 10:
			case 11:
			case 12:
				if ( $reversed ) {
					$month = 10;
				} else {
					$month = 1;
					$year ++;
				}
				break;
		}
		$datetime = new \DateTime( "$year-$month-01 00:00:00", new \DateTimeZone( wc_timezone_string() ) );
		if ( $reversed ) {
			$timestamp                   = (int) $datetime->format( 'U' );
			$end_of_prev_month_timestamp = $timestamp - 1;
			$datetime->setTimestamp( $end_of_prev_month_timestamp );
		}

		return $datetime;
	}

	/**
	 * Return a new DateTime object representing the next year start, or previous year end if reversed.
	 *
	 * @param DateTime $datetime Date and time.
	 * @param bool     $reversed Going backwards in time instead of forward.
	 * @return DateTime
	 */
	public static function next_year_start( $datetime, $reversed = false ) {
		$year_increment = 1;
		$year           = (int) $datetime->format( 'Y' );
		$month          = '01';
		$day            = '01';

		if ( $reversed ) {
			$datetime                   = new \DateTime( "$year-$month-$day 00:00:00", new \DateTimeZone( wc_timezone_string() ) );
			$timestamp                  = (int) $datetime->format( 'U' );
			$end_of_prev_year_timestamp = $timestamp - 1;
			$datetime->setTimestamp( $end_of_prev_year_timestamp );
		} else {
			$year    += $year_increment;
			$datetime = new \DateTime( "$year-$month-$day 00:00:00", new \DateTimeZone( wc_timezone_string() ) );
		}

		return $datetime;
	}

	/**
	 * Returns beginning of next time interval for provided DateTime.
	 *
	 * E.g. for current DateTime, beginning of next day, week, quarter, etc.
	 *
	 * @param DateTime $datetime      Date and time.
	 * @param string   $time_interval Time interval, e.g. week, day, hour.
	 * @param bool     $reversed Going backwards in time instead of forward.
	 * @return DateTime
	 */
	public static function iterate( $datetime, $time_interval, $reversed = false ) {
		return call_user_func( array( __CLASS__, "next_{$time_interval}_start" ), $datetime, $reversed );
	}

	/**
	 * Returns expected number of items on the page in case of date ordering.
	 *
	 * @param int $expected_interval_count Expected number of intervals in total.
	 * @param int $items_per_page          Number of items per page.
	 * @param int $page_no                 Page number.
	 *
	 * @return float|int
	 */
	public static function expected_intervals_on_page( $expected_interval_count, $items_per_page, $page_no ) {
		$total_pages = (int) ceil( $expected_interval_count / $items_per_page );
		if ( $page_no < $total_pages ) {
			return $items_per_page;
		} elseif ( $page_no === $total_pages ) {
			return $expected_interval_count - ( $page_no - 1 ) * $items_per_page;
		} else {
			return 0;
		}
	}

	/**
	 * Returns true if there are any intervals that need to be filled in the response.
	 *
	 * @param int    $expected_interval_count Expected number of intervals in total.
	 * @param int    $db_records              Total number of records for given period in the database.
	 * @param int    $items_per_page          Number of items per page.
	 * @param int    $page_no                 Page number.
	 * @param string $order                   asc or desc.
	 * @param string $order_by                Column by which the result will be sorted.
	 * @param int    $intervals_count         Number of records for given (possibly shortened) time interval.
	 *
	 * @return bool
	 */
	public static function intervals_missing( $expected_interval_count, $db_records, $items_per_page, $page_no, $order, $order_by, $intervals_count ) {
		if ( $expected_interval_count <= $db_records ) {
			return false;
		}
		if ( 'date' === $order_by ) {
			$expected_intervals_on_page = self::expected_intervals_on_page( $expected_interval_count, $items_per_page, $page_no );
			return $intervals_count < $expected_intervals_on_page;
		}
		if ( 'desc' === $order ) {
			return $page_no > floor( $db_records / $items_per_page );
		}
		if ( 'asc' === $order ) {
			return $page_no <= ceil( ( $expected_interval_count - $db_records ) / $items_per_page );
		}
		// Invalid ordering.
		return false;
	}

	/**
	 * Normalize "*_between" parameters to "*_min" and "*_max" for numeric values
	 * and "*_after" and "*_before" for date values.
	 *
	 * @param array        $request Query params from REST API request.
	 * @param string|array $param_names One or more param names to handle. Should not include "_between" suffix.
	 * @param bool         $is_date Boolean if the param is date is related.
	 * @return array Normalized query values.
	 */
	public static function normalize_between_params( $request, $param_names, $is_date ) {
		if ( ! is_array( $param_names ) ) {
			$param_names = array( $param_names );
		}

		$normalized = array();

		foreach ( $param_names as $param_name ) {
			if ( ! is_array( $request[ $param_name . '_between' ] ) ) {
				continue;
			}

			$range = $request[ $param_name . '_between' ];

			if ( 2 !== count( $range ) ) {
				continue;
			}

			$min = $is_date ? '_after' : '_min';
			$max = $is_date ? '_before' : '_max';

			if ( $range[0] < $range[1] ) {
				$normalized[ $param_name . $min ] = $range[0];
				$normalized[ $param_name . $max ] = $range[1];
			} else {
				$normalized[ $param_name . $min ] = $range[1];
				$normalized[ $param_name . $max ] = $range[0];
			}
		}

		return $normalized;
	}

	/**
	 * Validate a "*_between" range argument (an array with 2 numeric items).
	 *
	 * @param  mixed           $value Parameter value.
	 * @param  WP_REST_Request $request REST Request.
	 * @param  string          $param Parameter name.
	 * @return WP_Error|boolean
	 */
	public static function rest_validate_between_numeric_arg( $value, $request, $param ) {
		if ( ! wp_is_numeric_array( $value ) ) {
			return new \WP_Error(
				'rest_invalid_param',
				/* translators: 1: parameter name */
				sprintf( __( '%1$s is not a numerically indexed array.', 'woocommerce' ), $param )
			);
		}

		if (
			2 !== count( $value ) ||
			! is_numeric( $value[0] ) ||
			! is_numeric( $value[1] )
		) {
			return new \WP_Error(
				'rest_invalid_param',
				/* translators: %s: parameter name */
				sprintf( __( '%s must contain 2 numbers.', 'woocommerce' ), $param )
			);
		}

		return true;
	}

	/**
	 * Validate a "*_between" range argument (an array with 2 date items).
	 *
	 * @param  mixed           $value Parameter value.
	 * @param  WP_REST_Request $request REST Request.
	 * @param  string          $param Parameter name.
	 * @return WP_Error|boolean
	 */
	public static function rest_validate_between_date_arg( $value, $request, $param ) {
		if ( ! wp_is_numeric_array( $value ) ) {
			return new \WP_Error(
				'rest_invalid_param',
				/* translators: 1: parameter name */
				sprintf( __( '%1$s is not a numerically indexed array.', 'woocommerce' ), $param )
			);
		}

		if (
			2 !== count( $value ) ||
			! rest_parse_date( $value[0] ) ||
			! rest_parse_date( $value[1] )
		) {
			return new \WP_Error(
				'rest_invalid_param',
				/* translators: %s: parameter name */
				sprintf( __( '%s must contain 2 valid dates.', 'woocommerce' ), $param )
			);
		}

		return true;
	}

	/**
	 * Get dates from a timeframe string.
	 *
	 * @param int           $timeframe Timeframe to use.  One of: last_week|last_month|last_quarter|last_6_months|last_year.
	 * @param DateTime|null $current_date DateTime of current date to compare.
	 * @return array
	 */
	public static function get_timeframe_dates( $timeframe, $current_date = null ) {
		if ( ! $current_date ) {
			$current_date = new \DateTime();
		}
		$current_year  = $current_date->format( 'Y' );
		$current_month = $current_date->format( 'm' );

		if ( 'last_week' === $timeframe ) {
			return array(
				'start' => $current_date->modify( 'last week monday' )->format( 'Y-m-d 00:00:00' ),
				'end'   => $current_date->modify( 'this sunday' )->format( 'Y-m-d 23:59:59' ),
			);
		}

		if ( 'last_month' === $timeframe ) {
			return array(
				'start' => $current_date->modify( 'first day of previous month' )->format( 'Y-m-d 00:00:00' ),
				'end'   => $current_date->modify( 'last day of this month' )->format( 'Y-m-d 23:59:59' ),
			);
		}

		if ( 'last_quarter' === $timeframe ) {
			switch ( $current_month ) {
				case $current_month >= 1 && $current_month <= 3:
					return array(
						'start' => ( $current_year - 1 ) . '-10-01 00:00:00',
						'end'   => ( $current_year - 1 ) . '-12-31 23:59:59',
					);
				case $current_month >= 4 && $current_month <= 6:
					return array(
						'start' => $current_year . '-01-01 00:00:00',
						'end'   => $current_year . '-03-31 23:59:59',
					);
				case $current_month >= 7 && $current_month <= 9:
					return array(
						'start' => $current_year . '-04-01 00:00:00',
						'end'   => $current_year . '-06-30 23:59:59',
					);
				case $current_month >= 10 && $current_month <= 12:
					return array(
						'start' => $current_year . '-07-01 00:00:00',
						'end'   => $current_year . '-09-31 23:59:59',
					);
			}
		}

		if ( 'last_6_months' === $timeframe ) {
			if ( $current_month >= 1 && $current_month <= 6 ) {
				return array(
					'start' => ( $current_year - 1 ) . '-07-01 00:00:00',
					'end'   => ( $current_year - 1 ) . '-12-31 23:59:59',
				);
			}
			return array(
				'start' => $current_year . '-01-01 00:00:00',
				'end'   => $current_year . '-06-30 23:59:59',
			);
		}

		if ( 'last_year' === $timeframe ) {
			return array(
				'start' => ( $current_year - 1 ) . '-01-01 00:00:00',
				'end'   => ( $current_year - 1 ) . '-12-31 23:59:59',
			);
		}

		return false;
	}
}
Admin/API/Reports/ExportableInterface.php000064400000001160152333011040014270 0ustar00<?php
/**
 * Reports Exportable Controller Interface
 */

namespace Automattic\WooCommerce\Admin\API\Reports;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WooCommerce Reports exportable controller interface.
 *
 * @since 3.5.0
 */
interface ExportableInterface {

	/**
	 * Get the column names for export.
	 *
	 * @return array Key value pair of Column ID => Label.
	 */
	public function get_export_columns();

	/**
	 * Get the column values for export.
	 *
	 * @param array $item Single report item/row.
	 * @return array Key value pair of Column ID => Value.
	 */
	public function prepare_item_for_export( $item );
}
Admin/API/Reports/Variations/DataStore.php000064400000042162152333011040014356 0ustar00<?php
/**
 * API\Reports\Variations\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Variations;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;

/**
 * API\Reports\Variations\DataStore.
 */
class DataStore extends ReportsDataStore implements DataStoreInterface {

	/**
	 * Table used to get the data.
	 *
	 * @var string
	 */
	protected static $table_name = 'wc_order_product_lookup';

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'variations';

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'date_start'   => 'strval',
		'date_end'     => 'strval',
		'product_id'   => 'intval',
		'variation_id' => 'intval',
		'items_sold'   => 'intval',
		'net_revenue'  => 'floatval',
		'orders_count' => 'intval',
		'name'         => 'strval',
		'price'        => 'floatval',
		'image'        => 'strval',
		'permalink'    => 'strval',
		'sku'          => 'strval',
	);

	/**
	 * Extended product attributes to include in the data.
	 *
	 * @var array
	 */
	protected $extended_attributes = array(
		'name',
		'price',
		'image',
		'permalink',
		'stock_status',
		'stock_quantity',
		'low_stock_amount',
		'sku',
	);

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'variations';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$table_name           = self::get_db_table_name();
		$this->report_columns = array(
			'product_id'   => 'product_id',
			'variation_id' => 'variation_id',
			'items_sold'   => 'SUM(product_qty) as items_sold',
			'net_revenue'  => 'SUM(product_net_revenue) AS net_revenue',
			'orders_count' => "COUNT(DISTINCT {$table_name}.order_id) as orders_count",
		);
	}

	/**
	 * Fills FROM clause of SQL request based on user supplied parameters.
	 *
	 * @param array  $query_args Parameters supplied by the user.
	 * @param string $arg_name   Target of the JOIN sql param.
	 */
	protected function add_from_sql_params( $query_args, $arg_name ) {
		global $wpdb;

		if ( 'sku' !== $query_args['orderby'] ) {
			return;
		}

		$table_name = self::get_db_table_name();
		$join       = "LEFT JOIN {$wpdb->postmeta} AS postmeta ON {$table_name}.variation_id = postmeta.post_id AND postmeta.meta_key = '_sku'";

		if ( 'inner' === $arg_name ) {
			$this->subquery->add_sql_clause( 'join', $join );
		} else {
			$this->add_sql_clause( 'join', $join );
		}
	}

	/**
	 * Generate a subquery for order_item_id based on the attribute filters.
	 *
	 * @param array $query_args Query arguments supplied by the user.
	 * @return string
	 */
	protected function get_order_item_by_attribute_subquery( $query_args ) {
		$order_product_lookup_table = self::get_db_table_name();
		$attribute_subqueries       = $this->get_attribute_subqueries( $query_args, $order_product_lookup_table );

		if ( $attribute_subqueries['join'] && $attribute_subqueries['where'] ) {
			// Perform a subquery for DISTINCT order items that match our attribute filters.
			$attr_subquery = new SqlQuery( $this->context . '_attribute_subquery' );
			$attr_subquery->add_sql_clause( 'select', "DISTINCT {$order_product_lookup_table}.order_item_id" );
			$attr_subquery->add_sql_clause( 'from', $order_product_lookup_table );

			if ( $this->should_exclude_simple_products( $query_args ) ) {
				$attr_subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id != 0" );
			}

			foreach ( $attribute_subqueries['join'] as $attribute_join ) {
				$attr_subquery->add_sql_clause( 'join', $attribute_join );
			}

			$operator = $this->get_match_operator( $query_args );
			$attr_subquery->add_sql_clause( 'where', 'AND (' . implode( " {$operator} ", $attribute_subqueries['where'] ) . ')' );

			return "AND {$order_product_lookup_table}.order_item_id IN ({$attr_subquery->get_query_statement()})";
		}

		return false;
	}

	/**
	 * Updates the database query with parameters used for Products report: categories and order status.
	 *
	 * @param array $query_args Query arguments supplied by the user.
	 */
	protected function add_sql_query_params( $query_args ) {
		global $wpdb;
		$order_product_lookup_table = self::get_db_table_name();
		$order_stats_lookup_table   = $wpdb->prefix . 'wc_order_stats';
		$order_item_meta_table      = $wpdb->prefix . 'woocommerce_order_itemmeta';
		$where_subquery             = array();

		$this->add_time_period_sql_params( $query_args, $order_product_lookup_table );
		$this->get_limit_sql_params( $query_args );
		$this->add_order_by_sql_params( $query_args );

		$included_variations = $this->get_included_variations( $query_args );
		if ( $included_variations > 0 ) {
			$this->add_from_sql_params( $query_args, 'outer' );
		} else {
			$this->add_from_sql_params( $query_args, 'inner' );
		}

		$included_products = $this->get_included_products( $query_args );
		if ( $included_products ) {
			$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" );
		}

		$excluded_products = $this->get_excluded_products( $query_args );
		if ( $excluded_products ) {
			$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id NOT IN ({$excluded_products})" );
		}

		if ( $included_variations ) {
			$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id IN ({$included_variations})" );
		} elseif ( ! $included_products ) {
			if ( $this->should_exclude_simple_products( $query_args ) ) {
				$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id != 0" );
			}
		}

		$order_status_filter = $this->get_status_subquery( $query_args );
		if ( $order_status_filter ) {
			$this->subquery->add_sql_clause( 'join', "JOIN {$order_stats_lookup_table} ON {$order_product_lookup_table}.order_id = {$order_stats_lookup_table}.order_id" );
			$this->subquery->add_sql_clause( 'where', "AND ( {$order_status_filter} )" );
		}

		$attribute_order_items_subquery = $this->get_order_item_by_attribute_subquery( $query_args );
		if ( $attribute_order_items_subquery ) {
			// JOIN on product lookup if we haven't already.
			if ( ! $order_status_filter ) {
				$this->subquery->add_sql_clause( 'join', "JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id" );
			}

			// Add subquery for matching attributes to WHERE.
			$this->subquery->add_sql_clause( 'where', $attribute_order_items_subquery );
		}

		if ( 0 < count( $where_subquery ) ) {
			$operator = $this->get_match_operator( $query_args );
			$this->subquery->add_sql_clause( 'where', 'AND (' . implode( " {$operator} ", $where_subquery ) . ')' );
		}
	}

	/**
	 * Maps ordering specified by the user to columns in the database/fields in the data.
	 *
	 * @param string $order_by Sorting criterion.
	 *
	 * @return string
	 */
	protected function normalize_order_by( $order_by ) {
		if ( 'date' === $order_by ) {
			return self::get_db_table_name() . '.date_created';
		}
		if ( 'sku' === $order_by ) {
			return 'meta_value';
		}

		return $order_by;
	}

	/**
	 * Enriches the product data with attributes specified by the extended_attributes.
	 *
	 * @param array $products_data Product data.
	 * @param array $query_args Query parameters.
	 */
	protected function include_extended_info( &$products_data, $query_args ) {
		foreach ( $products_data as $key => $product_data ) {
			$extended_info = new \ArrayObject();
			if ( $query_args['extended_info'] ) {
				$extended_attributes = apply_filters( 'woocommerce_rest_reports_variations_extended_attributes', $this->extended_attributes, $product_data );
				$parent_product      = wc_get_product( $product_data['product_id'] );
				$attributes          = array();

				// Base extended info off the parent variable product if the variation ID is 0.
				// This is caused by simple products with prior sales being converted into variable products.
				// See: https://github.com/woocommerce/woocommerce-admin/issues/2719.
				$variation_id      = (int) $product_data['variation_id'];
				$variation_product = ( 0 === $variation_id ) ? $parent_product : wc_get_product( $variation_id );

				// Fall back to the parent product if the variation can't be found.
				$extended_attributes_product = is_a( $variation_product, 'WC_Product' ) ? $variation_product : $parent_product;
				// If both product and variation is not found, set deleted to true.
				if ( ! $extended_attributes_product ) {
					$extended_info['deleted'] = true;
				}
				foreach ( $extended_attributes as $extended_attribute ) {
					$function = 'get_' . $extended_attribute;
					if ( is_callable( array( $extended_attributes_product, $function ) ) ) {
						$value                                = $extended_attributes_product->{$function}();
						$extended_info[ $extended_attribute ] = $value;
					}
				}

				// If this is a variation, add its attributes.
				// NOTE: We don't fall back to the parent product here because it will include all possible attribute options.
				if (
					0 < $variation_id &&
					is_callable( array( $variation_product, 'get_variation_attributes' ) )
				) {
					$variation_attributes = $variation_product->get_variation_attributes();

					foreach ( $variation_attributes as $attribute_name => $attribute ) {
						$name         = str_replace( 'attribute_', '', $attribute_name );
						$option_term  = get_term_by( 'slug', $attribute, $name );
						$attributes[] = array(
							'id'     => wc_attribute_taxonomy_id_by_name( $name ),
							'name'   => str_replace( 'pa_', '', $name ),
							'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute,
						);
					}
				}

				$extended_info['attributes'] = $attributes;

				// If there is no set low_stock_amount, use the one in user settings.
				if ( '' === $extended_info['low_stock_amount'] ) {
					$extended_info['low_stock_amount'] = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
				}
				$extended_info = $this->cast_numbers( $extended_info );
			}
			$products_data[ $key ]['extended_info'] = $extended_info;
		}
	}

	/**
	 * Returns if simple products should be excluded from the report.
	 *
	 * @internal
	 *
	 * @param array $query_args Query parameters.
	 *
	 * @return boolean
	 */
	protected function should_exclude_simple_products( array $query_args ) {
		return apply_filters( 'experimental_woocommerce_analytics_variations_should_exclude_simple_products', true, $query_args );
	}

	/**
	 * Fill missing extended_info.name for the deleted products.
	 *
	 * @param array $products Product data.
	 */
	protected function fill_deleted_product_name( array &$products ) {
		global $wpdb;
		$product_variation_ids = [];
		// Find products with missing extended_info.name.
		foreach ( $products as $key => $product ) {
			if ( ! isset( $product['extended_info']['name'] ) ) {
				$product_variation_ids[ $key ] = [
					'product_id'   => $product['product_id'],
					'variation_id' => $product['variation_id'],
				];
			}
		}

		if ( ! count( $product_variation_ids ) ) {
			return;
		}

		$where_clauses = implode(
			' or ',
			array_map(
				function( $ids ) {
					return "(
						product_lookup.product_id = {$ids['product_id']}
						and
						product_lookup.variation_id = {$ids['variation_id']}
                    )";
				},
				$product_variation_ids
			)
		);

		$query = "
			select
				product_lookup.product_id,
				product_lookup.variation_id,
				order_items.order_item_name
			from
				{$wpdb->prefix}wc_order_product_lookup as product_lookup
				left join {$wpdb->prefix}woocommerce_order_items as order_items
				on product_lookup.order_item_id = order_items.order_item_id
			where
				{$where_clauses}
			group by
				product_lookup.product_id,
				product_lookup.variation_id,
				order_items.order_item_name
		";

		// phpcs:ignore
		$results = $wpdb->get_results( $query );
		$index   = [];
		foreach ( $results as $result ) {
			$index[ $result->product_id . '_' . $result->variation_id ] = $result->order_item_name;
		}

		foreach ( $product_variation_ids as $product_key => $ids ) {
			$product   = $products[ $product_key ];
			$index_key = $product['product_id'] . '_' . $product['variation_id'];
			if ( isset( $index[ $index_key ] ) ) {
				$products[ $product_key ]['extended_info']['name'] = $index[ $index_key ];
			}
		}
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @param array $query_args Query parameters.
	 *
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page'           => get_option( 'posts_per_page' ),
			'page'               => 1,
			'order'              => 'DESC',
			'orderby'            => 'date',
			'before'             => TimeInterval::default_before(),
			'after'              => TimeInterval::default_after(),
			'fields'             => '*',
			'product_includes'   => array(),
			'variation_includes' => array(),
			'extended_info'      => false,
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$data = (object) array(
				'data'    => array(),
				'total'   => 0,
				'pages'   => 0,
				'page_no' => 0,
			);

			$selections          = $this->selected_columns( $query_args );
			$included_variations =
				( isset( $query_args['variation_includes'] ) && is_array( $query_args['variation_includes'] ) )
					? $query_args['variation_includes']
					: array();
			$params              = $this->get_limit_params( $query_args );
			$this->add_sql_query_params( $query_args );

			if ( count( $included_variations ) > 0 ) {
				$total_results = count( $included_variations );
				$total_pages   = (int) ceil( $total_results / $params['per_page'] );

				$this->subquery->clear_sql_clause( 'select' );
				$this->subquery->add_sql_clause( 'select', $selections );

				if ( 'date' === $query_args['orderby'] ) {
					$this->subquery->add_sql_clause( 'select', ", {$table_name}.date_created" );
				}

				$fields          = $this->get_fields( $query_args );
				$join_selections = $this->format_join_selections( $fields, array( 'variation_id' ) );
				$ids_table       = $this->get_ids_table( $included_variations, 'variation_id' );

				$this->add_sql_clause( 'select', $join_selections );
				$this->add_sql_clause( 'from', '(' );
				$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
				$this->add_sql_clause( 'from', ") AS {$table_name}" );
				$this->add_sql_clause(
					'right_join',
					"RIGHT JOIN ( {$ids_table} ) AS default_results
					ON default_results.variation_id = {$table_name}.variation_id"
				);

				$variations_query = $this->get_query_statement();
			} else {
				/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
				$db_records_count = (int) $wpdb->get_var(
					"SELECT COUNT(*) FROM (
						{$this->subquery->get_query_statement()}
					) AS tt"
				);
				/* phpcs:enable */

				$total_results = $db_records_count;
				$total_pages   = (int) ceil( $db_records_count / $params['per_page'] );

				if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
					return $data;
				}

				$this->subquery->clear_sql_clause( 'select' );
				$this->subquery->add_sql_clause( 'select', $selections );
				$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
				$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
				$variations_query = $this->subquery->get_query_statement();
			}

			/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
			$product_data = $wpdb->get_results(
				$variations_query,
				ARRAY_A
			);
			/* phpcs:enable */

			if ( null === $product_data ) {
				return $data;
			}

			$this->include_extended_info( $product_data, $query_args );

			if ( $query_args['extended_info'] ) {
				$this->fill_deleted_product_name( $product_data );
			}

			$product_data = array_map( array( $this, 'cast_numbers' ), $product_data );
			$data         = (object) array(
				'data'    => $product_data,
				'total'   => $total_results,
				'pages'   => $total_pages,
				'page_no' => (int) $query_args['page'],
			);

			$this->set_cached_data( $cache_key, $data );
		}

		return $data;
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		$this->subquery = new SqlQuery( $this->context . '_subquery' );
		$this->subquery->add_sql_clause( 'select', 'product_id' );
		$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
		$this->subquery->add_sql_clause( 'group_by', 'product_id, variation_id' );
	}
}
Admin/API/Reports/Variations/Controller.php000064400000036475152333011040014625 0ustar00<?php
/**
 * REST API Reports products controller
 *
 * Handles requests to the /reports/products endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Variations;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;

/**
 * REST API Reports products controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends ReportsController implements ExportableInterface {
	/**
	 * Exportable traits.
	 */
	use ExportableTraits;

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/variations';

	/**
	 * Mapping between external parameter name and name used in query class.
	 *
	 * @var array
	 */
	protected $param_mapping = array(
		'variations' => 'variation_includes',
	);

	/**
	 * Get items.
	 *
	 * @param WP_REST_Request $request Request data.
	 *
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$args = array();
		/**
		 * Experimental: Filter the list of parameters provided when querying data from the data store.
		 *
		 * @ignore
		 *
		 * @param array $collection_params List of parameters.
		 */
		$collection_params = apply_filters(
			'experimental_woocommerce_analytics_variations_collection_params',
			$this->get_collection_params()
		);
		$registered        = array_keys( $collection_params );
		foreach ( $registered as $param_name ) {
			if ( isset( $request[ $param_name ] ) ) {
				if ( isset( $this->param_mapping[ $param_name ] ) ) {
					$args[ $this->param_mapping[ $param_name ] ] = $request[ $param_name ];
				} else {
					$args[ $param_name ] = $request[ $param_name ];
				}
			}
		}

		$reports       = new Query( $args );
		$products_data = $reports->get_data();

		$data = array();

		foreach ( $products_data->data as $product_data ) {
			$item   = $this->prepare_item_for_response( $product_data, $request );
			$data[] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $data );
		$response->header( 'X-WP-Total', (int) $products_data->total );
		$response->header( 'X-WP-TotalPages', (int) $products_data->pages );

		$page      = $products_data->page_no;
		$max_pages = $products_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param array           $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $report ) );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_variations', $response, $report, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param array $object Object data.
	 * @return array        Links for the given post.
	 */
	protected function prepare_links( $object ) {
		$links = array(
			'product'   => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, 'products', $object['product_id'] ) ),
			),
			'variation' => array(
				'href' => rest_url( sprintf( '/%s/%s/%d/%s/%d', $this->namespace, 'products', $object['product_id'], 'variation', $object['variation_id'] ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_varitations',
			'type'       => 'object',
			'properties' => array(
				'product_id'    => array(
					'type'        => 'integer',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Product ID.', 'woocommerce' ),
				),
				'variation_id'  => array(
					'type'        => 'integer',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Product ID.', 'woocommerce' ),
				),
				'items_sold'    => array(
					'type'        => 'integer',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Number of items sold.', 'woocommerce' ),
				),
				'net_revenue'   => array(
					'type'        => 'number',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Total Net sales of all items sold.', 'woocommerce' ),
				),
				'orders_count'  => array(
					'type'        => 'integer',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Number of orders product appeared in.', 'woocommerce' ),
				),
				'extended_info' => array(
					'name'             => array(
						'type'        => 'string',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product name.', 'woocommerce' ),
					),
					'price'            => array(
						'type'        => 'number',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product price.', 'woocommerce' ),
					),
					'image'            => array(
						'type'        => 'string',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product image.', 'woocommerce' ),
					),
					'permalink'        => array(
						'type'        => 'string',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product link.', 'woocommerce' ),
					),
					'attributes'       => array(
						'type'        => 'array',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product attributes.', 'woocommerce' ),
					),
					'stock_status'     => array(
						'type'        => 'string',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product inventory status.', 'woocommerce' ),
					),
					'stock_quantity'   => array(
						'type'        => 'integer',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product inventory quantity.', 'woocommerce' ),
					),
					'low_stock_amount' => array(
						'type'        => 'integer',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product inventory threshold for low stock.', 'woocommerce' ),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                      = array();
		$params['context']           = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']              = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']          = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']             = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']            = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['match']             = array(
			'description'       => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'all',
			'enum'              => array(
				'all',
				'any',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']             = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']           = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'date',
				'net_revenue',
				'orders_count',
				'items_sold',
				'sku',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['product_includes']  = array(
			'description'       => __( 'Limit result set to items that have the specified parent product(s).', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['product_excludes']  = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified parent product(s).', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['variations']        = array(
			'description'       => __( 'Limit result to items with specified variation ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['extended_info']     = array(
			'description'       => __( 'Add additional piece of info about each variation to the report.', 'woocommerce' ),
			'type'              => 'boolean',
			'default'           => false,
			'sanitize_callback' => 'wc_string_to_bool',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['attribute_is']      = array(
			'description'       => __( 'Limit result set to variations that include the specified attributes.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'array',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['attribute_is_not']  = array(
			'description'       => __( 'Limit result set to variations that don\'t include the specified attributes.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'array',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['category_includes'] = array(
			'description'       => __( 'Limit result set to variations in the specified categories.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['category_excludes'] = array(
			'description'       => __( 'Limit result set to variations not in the specified categories.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get stock status column export value.
	 *
	 * @param array $status Stock status from report row.
	 * @return string
	 */
	protected function get_stock_status( $status ) {
		$statuses = wc_get_product_stock_status_options();

		return isset( $statuses[ $status ] ) ? $statuses[ $status ] : '';
	}

	/**
	 * Get the column names for export.
	 *
	 * @return array Key value pair of Column ID => Label.
	 */
	public function get_export_columns() {
		$export_columns = array(
			'product_name' => __( 'Product / Variation title', 'woocommerce' ),
			'sku'          => __( 'SKU', 'woocommerce' ),
			'items_sold'   => __( 'Items sold', 'woocommerce' ),
			'net_revenue'  => __( 'N. Revenue', 'woocommerce' ),
			'orders_count' => __( 'Orders', 'woocommerce' ),
		);

		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			$export_columns['stock_status'] = __( 'Status', 'woocommerce' );
			$export_columns['stock']        = __( 'Stock', 'woocommerce' );
		}

		return $export_columns;
	}

	/**
	 * Get the column values for export.
	 *
	 * @param array $item Single report item/row.
	 * @return array Key value pair of Column ID => Row Value.
	 */
	public function prepare_item_for_export( $item ) {
		$export_item = array(
			'product_name' => $item['extended_info']['name'],
			'sku'          => $item['extended_info']['sku'],
			'items_sold'   => $item['items_sold'],
			'net_revenue'  => self::csv_number_format( $item['net_revenue'] ),
			'orders_count' => $item['orders_count'],
		);

		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			$export_item['stock_status'] = $this->get_stock_status( $item['extended_info']['stock_status'] );
			$export_item['stock']        = $item['extended_info']['stock_quantity'];
		}

		return $export_item;
	}
}
Admin/API/Reports/Variations/Query.php000064400000002367152333011040013600 0ustar00<?php
/**
 * Class for parameter-based Products Report querying
 *
 * Example usage:
 * $args = array(
 *          'before'       => '2018-07-19 00:00:00',
 *          'after'        => '2018-07-05 00:00:00',
 *          'page'         => 2,
 *          'categories'   => array(15, 18),
 *          'products'     => array(1,2,3)
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Variations\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Variations;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Variations\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Products report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array();
	}

	/**
	 * Get product data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_variations_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-variations' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_variations_select_query', $results, $args );
	}
}
Admin/API/Reports/Variations/Stats/Segmenter.php000064400000017671152333011040015526 0ustar00<?php
/**
 * Class for adding segmenting support without cluttering the data stores.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Segmenter as ReportsSegmenter;
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;

/**
 * Date & time interval and numeric range handling class for Reporting API.
 */
class Segmenter extends ReportsSegmenter {

	/**
	 * Returns column => query mapping to be used for product-related product-level segmenting query
	 * (e.g. products sold, revenue from product X when segmenting by category).
	 *
	 * @param string $products_table Name of SQL table containing the product-level segmenting info.
	 *
	 * @return array Column => SELECT query mapping.
	 */
	protected function get_segment_selections_product_level( $products_table ) {
		$columns_mapping = array(
			'items_sold'       => "SUM($products_table.product_qty) as items_sold",
			'net_revenue'      => "SUM($products_table.product_net_revenue ) AS net_revenue",
			'orders_count'     => "COUNT( DISTINCT $products_table.order_id ) AS orders_count",
			'variations_count' => "COUNT( DISTINCT $products_table.variation_id ) AS variations_count",
		);

		return $columns_mapping;
	}

	/**
	 * Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
	 *
	 * @param array  $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $segmenting_dimension_name Name of the segmenting dimension.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $totals_query Array of SQL clauses for totals query.
	 * @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
	 *
	 * @return array
	 */
	protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) {
		global $wpdb;

		$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';

		// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
		// Product-level numbers.
		/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
		$segments_products = $wpdb->get_results(
			"SELECT
						$segmenting_groupby AS $segmenting_dimension_name
						{$segmenting_selections['product_level']}
					FROM
						$table_name
						$segmenting_from
						{$totals_query['from_clause']}
					WHERE
						1=1
						{$totals_query['where_time_clause']}
						{$totals_query['where_clause']}
						$segmenting_where
					GROUP BY
						$segmenting_groupby",
			ARRAY_A
		);
		/* phpcs:enable */

		$totals_segments = $this->merge_segment_totals_results( $segmenting_dimension_name, $segments_products, array() );
		return $totals_segments;
	}

	/**
	 * Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
	 *
	 * @param array  $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $segmenting_dimension_name Name of the segmenting dimension.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $intervals_query Array of SQL clauses for intervals query.
	 * @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
	 *
	 * @return array
	 */
	protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) {
		global $wpdb;

		$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';

		// LIMIT offset, rowcount needs to be updated to a multiple of the number of segments.
		preg_match( '/LIMIT (\d+)\s?,\s?(\d+)/', $intervals_query['limit'], $limit_parts );
		$segment_count    = count( $this->get_all_segments() );
		$orig_offset      = intval( $limit_parts[1] );
		$orig_rowcount    = intval( $limit_parts[2] );
		$segmenting_limit = $wpdb->prepare( 'LIMIT %d, %d', $orig_offset * $segment_count, $orig_rowcount * $segment_count );

		// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
		// Product-level numbers.
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$segments_products = $wpdb->get_results(
			"SELECT
						{$intervals_query['select_clause']} AS time_interval,
						$segmenting_groupby AS $segmenting_dimension_name
						{$segmenting_selections['product_level']}
					FROM
						$table_name
						$segmenting_from
						{$intervals_query['from_clause']}
					WHERE
						1=1
						{$intervals_query['where_time_clause']}
						{$intervals_query['where_clause']}
						$segmenting_where
					GROUP BY
						time_interval, $segmenting_groupby
					$segmenting_limit",
				// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			ARRAY_A
		);

		$intervals_segments = $this->merge_segment_intervals_results( $segmenting_dimension_name, $segments_products, array() );
		return $intervals_segments;
	}

	/**
	 * Return array of segments formatted for REST response.
	 *
	 * @param string $type Type of segments to return--'totals' or 'intervals'.
	 * @param array  $query_params SQL query parameter array.
	 * @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
	 *
	 * @return array
	 * @throws \Automattic\WooCommerce\Admin\API\Reports\ParameterException In case of segmenting by variations, when no parent product is specified.
	 */
	protected function get_segments( $type, $query_params, $table_name ) {
		global $wpdb;
		if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
			return array();
		}

		$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
		$unique_orders_table      = 'uniq_orders';
		$segmenting_where         = '';

		// Product, variation, and category are bound to product, so here product segmenting table is required,
		// while coupon and customer are bound to order, so we don't need the extra JOIN for those.
		// This also means that segment selections need to be calculated differently.
		if ( 'variation' === $this->query_args['segmentby'] ) {
			$product_level_columns     = $this->get_segment_selections_product_level( $product_segmenting_table );
			$segmenting_selections     = array(
				'product_level' => $this->prepare_selections( $product_level_columns ),
			);
			$this->report_columns      = $product_level_columns;
			$segmenting_from           = '';
			$segmenting_groupby        = $product_segmenting_table . '.variation_id';
			$segmenting_dimension_name = 'variation_id';

			// Restrict our search space for variation comparisons.
			if ( isset( $this->query_args['variation_includes'] ) ) {
				$variation_ids    = implode( ',', $this->get_all_segments() );
				$segmenting_where = " AND $product_segmenting_table.variation_id IN ( $variation_ids )";
			}

			$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
		}

		return $segments;
	}
}
Admin/API/Reports/Variations/Stats/Controller.php000064400000037762152333011040015723 0ustar00<?php
/**
 * REST API Reports variations stats controller
 *
 * Handles requests to the /reports/variations/stats endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;

/**
 * REST API Reports variations stats controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/variations/stats';

	/**
	 * Mapping between external parameter name and name used in query class.
	 *
	 * @var array
	 */
	protected $param_mapping = array(
		'variations' => 'variation_includes',
	);

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_filter( 'woocommerce_analytics_variations_stats_select_query', array( $this, 'set_default_report_data' ) );
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$query_args = array(
			'fields' => array(
				'items_sold',
				'net_revenue',
				'orders_count',
				'variations_count',
			),
		);
		/**
		 * Experimental: Filter the list of parameters provided when querying data from the data store.
		 *
		 * @ignore
		 *
		 * @param array $collection_params List of parameters.
		 */
		$collection_params = apply_filters( 'experimental_woocommerce_analytics_variations_stats_collection_params', $this->get_collection_params() );
		$registered        = array_keys( $collection_params );
		foreach ( $registered as $param_name ) {
			if ( isset( $request[ $param_name ] ) ) {
				if ( isset( $this->param_mapping[ $param_name ] ) ) {
					$query_args[ $this->param_mapping[ $param_name ] ] = $request[ $param_name ];
				} else {
					$query_args[ $param_name ] = $request[ $param_name ];
				}
			}
		}

		$query = new Query( $query_args );
		try {
			$report_data = $query->get_data();
		} catch ( ParameterException $e ) {
			return new \WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}

		$out_data = array(
			'totals'    => get_object_vars( $report_data->totals ),
			'intervals' => array(),
		);

		foreach ( $report_data->intervals as $interval_data ) {
			$item                    = $this->prepare_item_for_response( $interval_data, $request );
			$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $out_data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		$page      = $report_data->page_no;
		$max_pages = $report_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param Array           $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_variations_stats', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$data_values = array(
			'items_sold'   => array(
				'title'       => __( 'Variations Sold', 'woocommerce' ),
				'description' => __( 'Number of variation items sold.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
			),
			'net_revenue'  => array(
				'description' => __( 'Net sales.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'format'      => 'currency',
			),
			'orders_count' => array(
				'description' => __( 'Number of orders.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
		);

		$segments = array(
			'segments' => array(
				'description' => __( 'Reports data grouped by segment condition.', 'woocommerce' ),
				'type'        => 'array',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'items'       => array(
					'type'       => 'object',
					'properties' => array(
						'segment_id'    => array(
							'description' => __( 'Segment identificator.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'segment_label' => array(
							'description' => __( 'Human readable segment label, either product or variation name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
							'enum'        => array( 'day', 'week', 'month', 'year' ),
						),
						'subtotals'     => array(
							'description' => __( 'Interval subtotals.', 'woocommerce' ),
							'type'        => 'object',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
							'properties'  => $data_values,
						),
					),
				),
			),
		);

		$totals = array_merge( $data_values, $segments );

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_variations_stats',
			'type'       => 'object',
			'properties' => array(
				'totals'    => array(
					'description' => __( 'Totals data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'properties'  => $totals,
				),
				'intervals' => array(
					'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'interval'       => array(
								'description' => __( 'Type of interval.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'enum'        => array( 'day', 'week', 'month', 'year' ),
							),
							'date_start'     => array(
								'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_start_gmt' => array(
								'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end'       => array(
								'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end_gmt'   => array(
								'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'subtotals'      => array(
								'description' => __( 'Interval subtotals.', 'woocommerce' ),
								'type'        => 'object',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'properties'  => $totals,
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Set the default results to 0 if API returns an empty array
	 *
	 * @param Mixed $results Report data.
	 * @return object
	 */
	public function set_default_report_data( $results ) {
		if ( empty( $results ) ) {
			$results                       = new \stdClass();
			$results->total                = 0;
			$results->totals               = new \stdClass();
			$results->totals->items_sold   = 0;
			$results->totals->net_revenue  = 0;
			$results->totals->orders_count = 0;
			$results->intervals            = array();
			$results->pages                = 1;
			$results->page_no              = 1;
		}
		return $results;
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                      = array();
		$params['context']           = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']              = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']          = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']             = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']            = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['match']             = array(
			'description'       => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'all',
			'enum'              => array(
				'all',
				'any',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']             = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']           = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'date',
				'net_revenue',
				'coupons',
				'refunds',
				'shipping',
				'taxes',
				'net_revenue',
				'orders_count',
				'items_sold',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['interval']          = array(
			'description'       => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'week',
			'enum'              => array(
				'hour',
				'day',
				'week',
				'month',
				'quarter',
				'year',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['category_includes'] = array(
			'description'       => __( 'Limit result to items from the specified categories.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['category_excludes'] = array(
			'description'       => __( 'Limit result set to variations not in the specified categories.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['product_includes']  = array(
			'description'       => __( 'Limit result set to items that have the specified parent product(s).', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['product_excludes']  = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified parent product(s).', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['variations']        = array(
			'description'       => __( 'Limit result to items with specified variation ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['segmentby']         = array(
			'description'       => __( 'Segment the response by additional constraint.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array(
				'product',
				'category',
				'variation',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['fields']            = array(
			'description'       => __( 'Limit stats fields to the specified items.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['attribute_is']      = array(
			'description'       => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'array',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['attribute_is_not']  = array(
			'description'       => __( 'Limit result set to orders that don\'t include products with the specified attributes.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'array',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
Admin/API/Reports/Variations/Stats/Query.php000064400000002447152333011040014675 0ustar00<?php
/**
 * Class for parameter-based Variations Stats Report querying
 *
 * Example usage:
 * $args = array(
 *          'before'       => '2018-07-19 00:00:00',
 *          'after'        => '2018-07-05 00:00:00',
 *          'page'         => 2,
 *          'categories'   => array(15, 18),
 *          'product_ids'  => array(1,2,3)
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Variations\Stats\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Variations\Stats\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Products report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array();
	}

	/**
	 * Get variations data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_variations_stats_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-variations-stats' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_variations_stats_select_query', $results, $args );
	}

}
Admin/API/Reports/Variations/Stats/DataStore.php000064400000026362152333011040015460 0ustar00<?php
/**
 * API\Reports\Products\Stats\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Variations\DataStore as VariationsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;

/**
 * API\Reports\Variations\Stats\DataStore.
 */
class DataStore extends VariationsDataStore implements DataStoreInterface {

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'items_sold'       => 'intval',
		'net_revenue'      => 'floatval',
		'orders_count'     => 'intval',
		'variations_count' => 'intval',
	);

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'variations_stats';

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'variations_stats';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$table_name           = self::get_db_table_name();
		$this->report_columns = array(
			'items_sold'       => 'SUM(product_qty) as items_sold',
			'net_revenue'      => 'SUM(product_net_revenue) AS net_revenue',
			'orders_count'     => "COUNT( DISTINCT ( CASE WHEN product_gross_revenue >= 0 THEN {$table_name}.order_id END ) ) as orders_count",
			'variations_count' => 'COUNT(DISTINCT variation_id) as variations_count',
		);
	}

	/**
	 * Updates the database query with parameters used for Products Stats report: categories and order status.
	 *
	 * @param array $query_args       Query arguments supplied by the user.
	 */
	protected function update_sql_query_params( $query_args ) {
		global $wpdb;

		$products_where_clause      = '';
		$products_from_clause       = '';
		$where_subquery             = array();
		$order_product_lookup_table = self::get_db_table_name();
		$order_item_meta_table      = $wpdb->prefix . 'woocommerce_order_itemmeta';

		$included_products = $this->get_included_products( $query_args );
		if ( $included_products ) {
			$products_where_clause .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";
		}

		$excluded_products = $this->get_excluded_products( $query_args );
		if ( $excluded_products ) {
			$products_where_clause .= "AND {$order_product_lookup_table}.product_id NOT IN ({$excluded_products})";
		}

		$included_variations = $this->get_included_variations( $query_args );
		if ( $included_variations ) {
			$products_where_clause .= " AND {$order_product_lookup_table}.variation_id IN ({$included_variations})";
		} elseif ( $this->should_exclude_simple_products( $query_args ) ) {
			$products_where_clause .= " AND {$order_product_lookup_table}.variation_id != 0";
		}

		$order_status_filter = $this->get_status_subquery( $query_args );
		if ( $order_status_filter ) {
			$products_from_clause  .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
			$products_where_clause .= " AND ( {$order_status_filter} )";
		}

		$attribute_order_items_subquery = $this->get_order_item_by_attribute_subquery( $query_args );
		if ( $attribute_order_items_subquery ) {
			// JOIN on product lookup if we haven't already.
			if ( ! $order_status_filter ) {
				$products_from_clause .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
			}

			// Add subquery for matching attributes to WHERE.
			$products_where_clause .= $attribute_order_items_subquery;
		}

		if ( 0 < count( $where_subquery ) ) {
			$operator               = $this->get_match_operator( $query_args );
			$products_where_clause .= 'AND (' . implode( " {$operator} ", $where_subquery ) . ')';
		}

		$this->add_time_period_sql_params( $query_args, $order_product_lookup_table );
		$this->total_query->add_sql_clause( 'where', $products_where_clause );
		$this->total_query->add_sql_clause( 'join', $products_from_clause );

		$this->add_intervals_sql_params( $query_args, $order_product_lookup_table );
		$this->interval_query->add_sql_clause( 'where', $products_where_clause );
		$this->interval_query->add_sql_clause( 'join', $products_from_clause );
		$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
	}

	/**
	 * Returns if simple products should be excluded from the report.
	 *
	 * @internal
	 *
	 * @param array $query_args Query parameters.
	 *
	 * @return boolean
	 */
	protected function should_exclude_simple_products( array $query_args ) {
		return apply_filters( 'experimental_woocommerce_analytics_variations_stats_should_exclude_simple_products', true, $query_args );
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @since 3.5.0
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page'           => get_option( 'posts_per_page' ),
			'page'               => 1,
			'order'              => 'DESC',
			'orderby'            => 'date',
			'before'             => TimeInterval::default_before(),
			'after'              => TimeInterval::default_after(),
			'fields'             => '*',
			'category_includes'  => array(),
			'interval'           => 'week',
			'product_includes'   => array(),
			'variation_includes' => array(),
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$selections = $this->selected_columns( $query_args );
			$params     = $this->get_limit_params( $query_args );

			$this->update_sql_query_params( $query_args );
			$this->get_limit_sql_params( $query_args );
			$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );

			/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
			$db_intervals = $wpdb->get_col(
				$this->interval_query->get_query_statement()
			);
			/* phpcs:enable */

			$db_interval_count       = count( $db_intervals );
			$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
			$total_pages             = (int) ceil( $expected_interval_count / $params['per_page'] );
			if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
				return array();
			}

			$intervals = array();
			$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
			$this->total_query->add_sql_clause( 'select', $selections );
			$this->total_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );

			/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
			$totals = $wpdb->get_results(
				$this->total_query->get_query_statement(),
				ARRAY_A
			);
			/* phpcs:enable */

			// @todo remove these assignements when refactoring segmenter classes to use query objects.
			$totals_query          = array(
				'from_clause'       => $this->total_query->get_sql_clause( 'join' ),
				'where_time_clause' => $this->total_query->get_sql_clause( 'where_time' ),
				'where_clause'      => $this->total_query->get_sql_clause( 'where' ),
			);
			$intervals_query       = array(
				'select_clause'     => $this->get_sql_clause( 'select' ),
				'from_clause'       => $this->interval_query->get_sql_clause( 'join' ),
				'where_time_clause' => $this->interval_query->get_sql_clause( 'where_time' ),
				'where_clause'      => $this->interval_query->get_sql_clause( 'where' ),
				'order_by'          => $this->get_sql_clause( 'order_by' ),
				'limit'             => $this->get_sql_clause( 'limit' ),
			);
			$segmenter             = new Segmenter( $query_args, $this->report_columns );
			$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );

			if ( null === $totals ) {
				return new \WP_Error( 'woocommerce_analytics_variations_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
			}

			$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
			$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
			$this->interval_query->add_sql_clause( 'select', ", MAX(${table_name}.date_created) AS datetime_anchor" );
			if ( '' !== $selections ) {
				$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
			}

			/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
			$intervals = $wpdb->get_results(
				$this->interval_query->get_query_statement(),
				ARRAY_A
			);
			/* phpcs:enable */

			if ( null === $intervals ) {
				return new \WP_Error( 'woocommerce_analytics_variations_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
			}

			$totals = (object) $this->cast_numbers( $totals[0] );

			$data = (object) array(
				'totals'    => $totals,
				'intervals' => $intervals,
				'total'     => $expected_interval_count,
				'pages'     => $total_pages,
				'page_no'   => (int) $query_args['page'],
			);

			if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
				$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
				$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
				$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
			} else {
				$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
			}
			$segmenter->add_intervals_segments( $data, $intervals_query, $table_name );
			$this->create_interval_subtotals( $data->intervals );

			$this->set_cached_data( $cache_key, $data );
		}

		return $data;
	}

	/**
	 * Normalizes order_by clause to match to SQL query.
	 *
	 * @param string $order_by Order by option requeste by user.
	 * @return string
	 */
	protected function normalize_order_by( $order_by ) {
		if ( 'date' === $order_by ) {
			return 'time_interval';
		}

		return $order_by;
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		unset( $this->subquery );
		$this->total_query = new SqlQuery( $this->context . '_total' );
		$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );

		$this->interval_query = new SqlQuery( $this->context . '_interval' );
		$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
		$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
	}
}
Admin/API/Reports/Revenue/Stats/Controller.php000064400000037041152333011040015203 0ustar00<?php
/**
 * REST API Reports revenue stats controller
 *
 * Handles requests to the /reports/revenue/stats endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Revenue\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Revenue\Query as RevenueQuery;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;

/**
 * REST API Reports revenue stats controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
	/**
	 * Exportable traits.
	 */
	use ExportableTraits;

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/revenue/stats';

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args                        = array();
		$args['before']              = $request['before'];
		$args['after']               = $request['after'];
		$args['interval']            = $request['interval'];
		$args['page']                = $request['page'];
		$args['per_page']            = $request['per_page'];
		$args['orderby']             = $request['orderby'];
		$args['order']               = $request['order'];
		$args['segmentby']           = $request['segmentby'];
		$args['fields']              = $request['fields'];
		$args['force_cache_refresh'] = $request['force_cache_refresh'];

		return $args;
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_items( $request ) {
		$query_args      = $this->prepare_reports_query( $request );
		$reports_revenue = new RevenueQuery( $query_args );
		try {
			$report_data = $reports_revenue->get_data();
		} catch ( ParameterException $e ) {
			return new \WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}

		$out_data = array(
			'totals'    => get_object_vars( $report_data->totals ),
			'intervals' => array(),
		);

		foreach ( $report_data->intervals as $interval_data ) {
			$item                    = $this->prepare_item_for_response( $interval_data, $request );
			$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $out_data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		$page      = $report_data->page_no;
		$max_pages = $report_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Get report items for export.
	 *
	 * Returns only the interval data.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response
	 */
	public function get_export_items( $request ) {
		$response  = $this->get_items( $request );
		$data      = $response->get_data();
		$intervals = $data['intervals'];

		$response->set_data( $intervals );

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param Array           $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_revenue_stats', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$data_values = array(
			'total_sales'    => array(
				'description' => __( 'Total sales.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
				'format'      => 'currency',
			),
			'net_revenue'    => array(
				'description' => __( 'Net sales.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
				'format'      => 'currency',
			),
			'coupons'        => array(
				'description' => __( 'Amount discounted by coupons.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'coupons_count'  => array(
				'description' => __( 'Unique coupons count.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'format'      => 'currency',
			),
			'shipping'       => array(
				'title'       => __( 'Shipping', 'woocommerce' ),
				'description' => __( 'Total of shipping.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
				'format'      => 'currency',
			),
			'taxes'          => array(
				'description' => __( 'Total of taxes.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'format'      => 'currency',
			),
			'refunds'        => array(
				'title'       => __( 'Returns', 'woocommerce' ),
				'description' => __( 'Total of returns.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
				'format'      => 'currency',
			),
			'orders_count'   => array(
				'description' => __( 'Number of orders.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'num_items_sold' => array(
				'description' => __( 'Items sold.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'products'       => array(
				'description' => __( 'Products sold.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'gross_sales'    => array(
				'description' => __( 'Gross sales.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
				'format'      => 'currency',
			),
		);

		$segments = array(
			'segments' => array(
				'description' => __( 'Reports data grouped by segment condition.', 'woocommerce' ),
				'type'        => 'array',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'items'       => array(
					'type'       => 'object',
					'properties' => array(
						'segment_id' => array(
							'description' => __( 'Segment identificator.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'subtotals'  => array(
							'description' => __( 'Interval subtotals.', 'woocommerce' ),
							'type'        => 'object',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
							'properties'  => $data_values,
						),
					),
				),
			),
		);

		$totals = array_merge( $data_values, $segments );

		// Products is not shown in intervals.
		unset( $data_values['products'] );

		$intervals = array_merge( $data_values, $segments );

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_revenue_stats',
			'type'       => 'object',
			'properties' => array(
				'totals'    => array(
					'description' => __( 'Totals data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'properties'  => $totals,
				),
				'intervals' => array(
					'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'interval'       => array(
								'description' => __( 'Type of interval.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'enum'        => array( 'day', 'week', 'month', 'year' ),
							),
							'date_start'     => array(
								'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_start_gmt' => array(
								'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end'       => array(
								'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end_gmt'   => array(
								'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'subtotals'      => array(
								'description' => __( 'Interval subtotals.', 'woocommerce' ),
								'type'        => 'object',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'properties'  => $intervals,
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params              = array();
		$params['context']   = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']      = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']  = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']     = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']    = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']     = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']   = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'date',
				'total_sales',
				'coupons',
				'refunds',
				'shipping',
				'taxes',
				'net_revenue',
				'orders_count',
				'items_sold',
				'gross_sales',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['interval']  = array(
			'description'       => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'week',
			'enum'              => array(
				'hour',
				'day',
				'week',
				'month',
				'quarter',
				'year',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['segmentby'] = array(
			'description'       => __( 'Segment the response by additional constraint.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array(
				'product',
				'category',
				'variation',
				'coupon',
				'customer_type', // new vs returning.
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get the column names for export.
	 *
	 * @return array Key value pair of Column ID => Label.
	 */
	public function get_export_columns() {
		return array(
			'date'         => __( 'Date', 'woocommerce' ),
			'orders_count' => __( 'Orders', 'woocommerce' ),
			'gross_sales'  => __( 'Gross sales', 'woocommerce' ),
			'refunds'      => __( 'Returns', 'woocommerce' ),
			'coupons'      => __( 'Coupons', 'woocommerce' ),
			'net_revenue'  => __( 'Net sales', 'woocommerce' ),
			'taxes'        => __( 'Taxes', 'woocommerce' ),
			'shipping'     => __( 'Shipping', 'woocommerce' ),
			'total_sales'  => __( 'Total sales', 'woocommerce' ),
		);
	}

	/**
	 * Get the column values for export.
	 *
	 * @param array $item Single report item/row.
	 * @return array Key value pair of Column ID => Row Value.
	 */
	public function prepare_item_for_export( $item ) {
		$subtotals = (array) $item['subtotals'];

		return array(
			'date'         => $item['date_start'],
			'orders_count' => $subtotals['orders_count'],
			'gross_sales'  => self::csv_number_format( $subtotals['gross_sales'] ),
			'refunds'      => self::csv_number_format( $subtotals['refunds'] ),
			'coupons'      => self::csv_number_format( $subtotals['coupons'] ),
			'net_revenue'  => self::csv_number_format( $subtotals['net_revenue'] ),
			'taxes'        => self::csv_number_format( $subtotals['taxes'] ),
			'shipping'     => self::csv_number_format( $subtotals['shipping'] ),
			'total_sales'  => self::csv_number_format( $subtotals['total_sales'] ),
		);
	}
}
Admin/API/Reports/Revenue/Query.php000064400000003100152333011040013054 0ustar00<?php
/**
 * Class for parameter-based Revenue Reports querying
 *
 * Example usage:
 * $args = array(
 *          'before' => '2018-07-19 00:00:00',
 *          'after'  => '2018-07-05 00:00:00',
 *          'interval' => 'week',
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Revenue\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Revenue;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Revenue\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Revenue report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array(
			'per_page' => get_option( 'posts_per_page' ), // not sure if this should be the default.
			'page'     => 1,
			'order'    => 'DESC',
			'orderby'  => 'date',
			'before'   => '',
			'after'    => '',
			'interval' => 'week',
			'fields'   => array(
				'orders_count',
				'num_items_sold',
				'total_sales',
				'coupons',
				'coupons_count',
				'refunds',
				'taxes',
				'shipping',
				'net_revenue',
				'gross_sales',
			),
		);
	}

	/**
	 * Get revenue data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_revenue_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-revenue-stats' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_revenue_select_query', $results, $args );
	}
}
Admin/API/Reports/Export/Controller.php000064400000015570152333011040013760 0ustar00<?php
/**
 * REST API Reports Export Controller
 *
 * Handles requests to:
 * - /reports/[report]/export
 * - /reports/[report]/export/[id]/status
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Export;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\ReportExporter;

/**
 * Reports Export controller.
 *
 * @internal
 * @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
 */
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/(?P<type>[a-z]+)/export';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'export_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_export_collection_params(),
				),
				'schema' => array( $this, 'get_export_public_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<export_id>[a-z0-9]+)/status',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'export_status' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_export_status_public_schema' ),
			)
		);
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	protected function get_export_collection_params() {
		$params                = array();
		$params['report_args'] = array(
			'description'       => __( 'Parameters to pass on to the exported report.', 'woocommerce' ),
			'type'              => 'object',
			'validate_callback' => 'rest_validate_request_arg', // @todo: use each controller's schema?
		);
		$params['email']       = array(
			'description'       => __( 'When true, email a link to download the export to the requesting user.', 'woocommerce' ),
			'type'              => 'boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);
		return $params;
	}

	/**
	 * Get the Report Export's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_export_public_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_export',
			'type'       => 'object',
			'properties' => array(
				'status'    => array(
					'description' => __( 'Export status.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'message'   => array(
					'description' => __( 'Export status message.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'export_id' => array(
					'description' => __( 'Export ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the Export status schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_export_status_public_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_export_status',
			'type'       => 'object',
			'properties' => array(
				'percent_complete' => array(
					'description' => __( 'Percentage complete.', 'woocommerce' ),
					'type'        => 'int',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'download_url'     => array(
					'description' => __( 'Export download URL.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Export data based on user request params.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function export_items( $request ) {
		$report_type = $request['type'];
		$report_args = empty( $request['report_args'] ) ? array() : $request['report_args'];
		$send_email  = isset( $request['email'] ) ? $request['email'] : false;

		$default_export_id = str_replace( '.', '', microtime( true ) );
		$export_id         = apply_filters( 'woocommerce_admin_export_id', $default_export_id );
		$export_id         = (string) sanitize_file_name( $export_id );

		$total_rows = ReportExporter::queue_report_export( $export_id, $report_type, $report_args, $send_email );

		if ( 0 === $total_rows ) {
			return rest_ensure_response(
				array(
					'message' => __( 'There is no data to export for the given request.', 'woocommerce' ),
				)
			);
		}

		ReportExporter::update_export_percentage_complete( $report_type, $export_id, 0 );

		$response = rest_ensure_response(
			array(
				'message'   => __( 'Your report file is being generated.', 'woocommerce' ),
				'export_id' => $export_id,
			)
		);

		// Include a link to the export status endpoint.
		$response->add_links(
			array(
				'status' => array(
					'href' => rest_url( sprintf( '%s/reports/%s/export/%s/status', $this->namespace, $report_type, $export_id ) ),
				),
			)
		);

		$data = $this->prepare_response_for_collection( $response );

		return rest_ensure_response( $data );
	}

	/**
	 * Export status based on user request params.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function export_status( $request ) {
		$report_type = $request['type'];
		$export_id   = $request['export_id'];
		$percentage  = ReportExporter::get_export_percentage_complete( $report_type, $export_id );

		if ( false === $percentage ) {
			return new \WP_Error(
				'woocommerce_admin_reports_export_invalid_id',
				__( 'Sorry, there is no export with that ID.', 'woocommerce' ),
				array( 'status' => 404 )
			);
		}

		$result = array(
			'percent_complete' => $percentage,
		);

		// @todo - add thing in the links below instead?
		if ( 100 === $percentage ) {
			$query_args = array(
				'action'   => ReportExporter::DOWNLOAD_EXPORT_ACTION,
				'filename' => "wc-{$report_type}-report-export-{$export_id}",
			);

			$result['download_url'] = add_query_arg( $query_args, admin_url() );
		}

		// Wrap the data in a response object.
		$response = rest_ensure_response( $result );
		// Include a link to the export status endpoint.
		$response->add_links(
			array(
				'self' => array(
					'href' => rest_url( sprintf( '%s/reports/%s/export/%s/status', $this->namespace, $report_type, $export_id ) ),
				),
			)
		);

		$data = $this->prepare_response_for_collection( $response );

		return rest_ensure_response( $data );
	}
}
Admin/API/Reports/DataStore.php000064400000142455152333011040012245 0ustar00<?php
/**
 * Admin\API\Reports\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;

/**
 * Admin\API\Reports\DataStore: Common parent for custom report data stores.
 */
class DataStore extends SqlQuery {

	/**
	 * Cache group for the reports.
	 *
	 * @var string
	 */
	protected $cache_group = 'reports';

	/**
	 * Time out for the cache.
	 *
	 * @var int
	 */
	protected $cache_timeout = 3600;

	/**
	 * Table used as a data store for this report.
	 *
	 * @var string
	 */
	protected static $table_name = '';

	/**
	 * Date field name.
	 *
	 * @var string
	 */
	protected $date_column_name = 'date_created';

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array();

	// @todo This does not really belong here, maybe factor out the comparison as separate class?
	/**
	 * Order by property, used in the cmp function.
	 *
	 * @var string
	 */
	private $order_by = '';

	/**
	 * Order property, used in the cmp function.
	 *
	 * @var string
	 */
	private $order = '';

	/**
	 * Query limit parameters.
	 *
	 * @var array
	 */
	private $limit_parameters = array();

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'reports';

	/**
	 * Subquery object for query nesting.
	 *
	 * @var SqlQuery
	 */
	protected $subquery;

	/**
	 * Totals query object.
	 *
	 * @var SqlQuery
	 */
	protected $total_query;

	/**
	 * Intervals query object.
	 *
	 * @var SqlQuery
	 */
	protected $interval_query;

	/**
	 * Refresh the cache for the current query when true.
	 *
	 * @var bool
	 */
	protected $force_cache_refresh = false;

	/**
	 * Include debugging information in the returned data when true.
	 *
	 * @var bool
	 */
	protected $debug_cache = true;

	/**
	 * Debugging information to include in the returned data.
	 *
	 * @var array
	 */
	protected $debug_cache_data = array();

	/**
	 * Class constructor.
	 */
	public function __construct() {
		self::set_db_table_name();
		$this->assign_report_columns();

		if ( property_exists( $this, 'report_columns' ) ) {
			$this->report_columns = apply_filters(
				'woocommerce_admin_report_columns',
				$this->report_columns,
				$this->context,
				self::get_db_table_name()
			);
		}

		// Utilize enveloped responses to include debugging info.
		// See https://querymonitor.com/blog/2021/05/debugging-wordpress-rest-api-requests/
		if ( isset( $_GET['_envelope'] ) ) {
			$this->debug_cache = true;
			add_filter( 'rest_envelope_response', array( $this, 'add_debug_cache_to_envelope' ), 999, 2 );
		}
	}

	/**
	 * Get table name from database class.
	 */
	public static function get_db_table_name() {
		global $wpdb;
		return isset( $wpdb->{static::$table_name} ) ? $wpdb->{static::$table_name} : $wpdb->prefix . static::$table_name;
	}

	/**
	 * Set table name from database class.
	 */
	protected static function set_db_table_name() {
		global $wpdb;
		if ( static::$table_name && ! isset( $wpdb->{static::$table_name} ) ) {
			$wpdb->{static::$table_name} = $wpdb->prefix . static::$table_name;
		}
	}

	/**
	 * Whether or not the report should use the caching layer.
	 *
	 * Provides an opportunity for plugins to prevent reports from using cache.
	 *
	 * @return boolean Whether or not to utilize caching.
	 */
	protected function should_use_cache() {
		/**
		 * Determines if a report will utilize caching.
		 *
		 * @param bool $use_cache Whether or not to use cache.
		 * @param string $cache_key The report's cache key. Used to identify the report.
		 */
		return (bool) apply_filters( 'woocommerce_analytics_report_should_use_cache', true, $this->cache_key );
	}

	/**
	 * Returns string to be used as cache key for the data.
	 *
	 * @param array $params Query parameters.
	 * @return string
	 */
	protected function get_cache_key( $params ) {
		if ( isset( $params['force_cache_refresh'] ) ) {
			if ( true === $params['force_cache_refresh'] ) {
				$this->force_cache_refresh = true;
			}

			// We don't want this param in the key.
			unset( $params['force_cache_refresh'] );
		}

		if ( true === $this->debug_cache ) {
			$this->debug_cache_data['query_args'] = $params;
		}

		return implode(
			'_',
			array(
				'wc_report',
				$this->cache_key,
				md5( wp_json_encode( $params ) ),
			)
		);
	}

	/**
	 * Wrapper around Cache::get().
	 *
	 * @param string $cache_key Cache key.
	 * @return mixed
	 */
	protected function get_cached_data( $cache_key ) {
		if ( true === $this->debug_cache ) {
			$this->debug_cache_data['should_use_cache']    = $this->should_use_cache();
			$this->debug_cache_data['force_cache_refresh'] = $this->force_cache_refresh;
			$this->debug_cache_data['cache_hit']           = false;
		}

		if ( $this->should_use_cache() && false === $this->force_cache_refresh ) {
			$cached_data = Cache::get( $cache_key );

			$cache_hit = false !== $cached_data;
			if ( true === $this->debug_cache ) {
				$this->debug_cache_data['cache_hit'] = $cache_hit;
			}

			return $cached_data;
		}

		// Cached item has now functionally been refreshed. Reset the option.
		$this->force_cache_refresh = false;

		return false;
	}

	/**
	 * Wrapper around Cache::set().
	 *
	 * @param string $cache_key Cache key.
	 * @param mixed  $value     New value.
	 * @return bool
	 */
	protected function set_cached_data( $cache_key, $value ) {
		if ( $this->should_use_cache() ) {
			return Cache::set( $cache_key, $value );
		}

		return true;
	}

	/**
	 * Add cache debugging information to an enveloped API response.
	 *
	 * @param array             $envelope
	 * @param \WP_REST_Response $response
	 *
	 * @return array
	 */
	public function add_debug_cache_to_envelope( $envelope, $response ) {
		if ( 0 !== strncmp( '/wc-analytics', $response->get_matched_route(), 13 ) ) {
			return $envelope;
		}

		if ( ! empty( $this->debug_cache_data ) ) {
			$envelope['debug_cache'] = $this->debug_cache_data;
		}

		return $envelope;
	}

	/**
	 * Compares two report data objects by pre-defined object property and ASC/DESC ordering.
	 *
	 * @param stdClass $a Object a.
	 * @param stdClass $b Object b.
	 * @return string
	 */
	private function interval_cmp( $a, $b ) {
		if ( '' === $this->order_by || '' === $this->order ) {
			return 0;
			// @todo Should return WP_Error here perhaps?
		}
		if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) {
			// As relative order is undefined in case of equality in usort, second-level sorting by date needs to be enforced
			// so that paging is stable.
			if ( $a['time_interval'] === $b['time_interval'] ) {
				return 0; // This should never happen.
			} elseif ( $a['time_interval'] > $b['time_interval'] ) {
				return 1;
			} elseif ( $a['time_interval'] < $b['time_interval'] ) {
				return -1;
			}
		} elseif ( $a[ $this->order_by ] > $b[ $this->order_by ] ) {
			return strtolower( $this->order ) === 'desc' ? -1 : 1;
		} elseif ( $a[ $this->order_by ] < $b[ $this->order_by ] ) {
			return strtolower( $this->order ) === 'desc' ? 1 : -1;
		}
	}

	/**
	 * Sorts intervals according to user's request.
	 *
	 * They are pre-sorted in SQL, but after adding gaps, they need to be sorted including the added ones.
	 *
	 * @param stdClass $data      Data object, must contain an array under $data->intervals.
	 * @param string   $sort_by   Ordering property.
	 * @param string   $direction DESC/ASC.
	 */
	protected function sort_intervals( &$data, $sort_by, $direction ) {
		$this->sort_array( $data->intervals, $sort_by, $direction );
	}

	/**
	 * Sorts array of arrays based on subarray key $sort_by.
	 *
	 * @param array  $arr       Array to sort.
	 * @param string $sort_by   Ordering property.
	 * @param string $direction DESC/ASC.
	 */
	protected function sort_array( &$arr, $sort_by, $direction ) {
		$this->order_by = $this->normalize_order_by( $sort_by );
		$this->order    = $direction;
		usort( $arr, array( $this, 'interval_cmp' ) );
	}

	/**
	 * Fills in interval gaps from DB with 0-filled objects.
	 *
	 * @param array    $db_intervals   Array of all intervals present in the db.
	 * @param DateTime $start_datetime Start date.
	 * @param DateTime $end_datetime   End date.
	 * @param string   $time_interval  Time interval, e.g. day, week, month.
	 * @param stdClass $data           Data with SQL extracted intervals.
	 * @return stdClass
	 */
	protected function fill_in_missing_intervals( $db_intervals, $start_datetime, $end_datetime, $time_interval, &$data ) {
		// @todo This is ugly and messy.
		$local_tz = new \DateTimeZone( wc_timezone_string() );
		// At this point, we don't know when we can stop iterating, as the ordering can be based on any value.
		$time_ids     = array_flip( wp_list_pluck( $data->intervals, 'time_interval' ) );
		$db_intervals = array_flip( $db_intervals );
		// Totals object used to get all needed properties.
		$totals_arr = get_object_vars( $data->totals );
		foreach ( $totals_arr as $key => $val ) {
			$totals_arr[ $key ] = 0;
		}
		// @todo Should 'products' be in intervals?
		unset( $totals_arr['products'] );
		while ( $start_datetime <= $end_datetime ) {
			$next_start = TimeInterval::iterate( $start_datetime, $time_interval );
			$time_id    = TimeInterval::time_interval_id( $time_interval, $start_datetime );
			// Either create fill-zero interval or use data from db.
			if ( $next_start > $end_datetime ) {
				$interval_end = $end_datetime->format( 'Y-m-d H:i:s' );
			} else {
				$prev_end_timestamp = (int) $next_start->format( 'U' ) - 1;
				$prev_end           = new \DateTime();
				$prev_end->setTimestamp( $prev_end_timestamp );
				$prev_end->setTimezone( $local_tz );
				$interval_end = $prev_end->format( 'Y-m-d H:i:s' );
			}
			if ( array_key_exists( $time_id, $time_ids ) ) {
				// For interval present in the db for this time frame, just fill in dates.
				$record               = &$data->intervals[ $time_ids[ $time_id ] ];
				$record['date_start'] = $start_datetime->format( 'Y-m-d H:i:s' );
				$record['date_end']   = $interval_end;
			} elseif ( ! array_key_exists( $time_id, $db_intervals ) ) {
				// For intervals present in the db outside of this time frame, do nothing.
				// For intervals not present in the db, fabricate it.
				$record_arr                  = array();
				$record_arr['time_interval'] = $time_id;
				$record_arr['date_start']    = $start_datetime->format( 'Y-m-d H:i:s' );
				$record_arr['date_end']      = $interval_end;
				$data->intervals[]           = array_merge( $record_arr, $totals_arr );
			}
			$start_datetime = $next_start;
		}
		return $data;
	}

	/**
	 * Converts input datetime parameters to local timezone. If there are no inputs from the user in query_args,
	 * uses default from $defaults.
	 *
	 * @param array $query_args Array of query arguments.
	 * @param array $defaults Array of default values.
	 */
	protected function normalize_timezones( &$query_args, $defaults ) {
		$local_tz = new \DateTimeZone( wc_timezone_string() );
		foreach ( array( 'before', 'after' ) as $query_arg_key ) {
			if ( isset( $query_args[ $query_arg_key ] ) && is_string( $query_args[ $query_arg_key ] ) ) {
				// Assume that unspecified timezone is a local timezone.
				$datetime = new \DateTime( $query_args[ $query_arg_key ], $local_tz );
				// In case timezone was forced by using +HH:MM, convert to local timezone.
				$datetime->setTimezone( $local_tz );
				$query_args[ $query_arg_key ] = $datetime;
			} elseif ( isset( $query_args[ $query_arg_key ] ) && is_a( $query_args[ $query_arg_key ], 'DateTime' ) ) {
				// In case timezone is in other timezone, convert to local timezone.
				$query_args[ $query_arg_key ]->setTimezone( $local_tz );
			} else {
				$query_args[ $query_arg_key ] = isset( $defaults[ $query_arg_key ] ) ? $defaults[ $query_arg_key ] : null;
			}
		}
	}

	/**
	 * Removes extra records from intervals so that only requested number of records get returned.
	 *
	 * @param stdClass $data           Data from whose intervals the records get removed.
	 * @param int      $page_no        Offset requested by the user.
	 * @param int      $items_per_page Number of records requested by the user.
	 * @param int      $db_interval_count Database interval count.
	 * @param int      $expected_interval_count Expected interval count on the output.
	 * @param string   $order_by Order by field.
	 * @param string   $order ASC or DESC.
	 */
	protected function remove_extra_records( &$data, $page_no, $items_per_page, $db_interval_count, $expected_interval_count, $order_by, $order ) {
		if ( 'date' === strtolower( $order_by ) ) {
			$offset = 0;
		} else {
			if ( 'asc' === strtolower( $order ) ) {
				$offset = ( $page_no - 1 ) * $items_per_page;
			} else {
				$offset = ( $page_no - 1 ) * $items_per_page - $db_interval_count;
			}
			$offset = $offset < 0 ? 0 : $offset;
		}
		$count = $expected_interval_count - ( $page_no - 1 ) * $items_per_page;
		if ( $count < 0 ) {
			$count = 0;
		} elseif ( $count > $items_per_page ) {
			$count = $items_per_page;
		}
		$data->intervals = array_slice( $data->intervals, $offset, $count );
	}

	/**
	 * Returns expected number of items on the page in case of date ordering.
	 *
	 * @param int $expected_interval_count Expected number of intervals in total.
	 * @param int $items_per_page          Number of items per page.
	 * @param int $page_no                 Page number.
	 *
	 * @return float|int
	 */
	protected function expected_intervals_on_page( $expected_interval_count, $items_per_page, $page_no ) {
		$total_pages = (int) ceil( $expected_interval_count / $items_per_page );
		if ( $page_no < $total_pages ) {
			return $items_per_page;
		} elseif ( $page_no === $total_pages ) {
			return $expected_interval_count - ( $page_no - 1 ) * $items_per_page;
		} else {
			return 0;
		}
	}

	/**
	 * Returns true if there are any intervals that need to be filled in the response.
	 *
	 * @param int    $expected_interval_count Expected number of intervals in total.
	 * @param int    $db_records              Total number of records for given period in the database.
	 * @param int    $items_per_page          Number of items per page.
	 * @param int    $page_no                 Page number.
	 * @param string $order                   asc or desc.
	 * @param string $order_by                Column by which the result will be sorted.
	 * @param int    $intervals_count         Number of records for given (possibly shortened) time interval.
	 *
	 * @return bool
	 */
	protected function intervals_missing( $expected_interval_count, $db_records, $items_per_page, $page_no, $order, $order_by, $intervals_count ) {
		if ( $expected_interval_count <= $db_records ) {
			return false;
		}
		if ( 'date' === $order_by ) {
			$expected_intervals_on_page = $this->expected_intervals_on_page( $expected_interval_count, $items_per_page, $page_no );
			return $intervals_count < $expected_intervals_on_page;
		}
		if ( 'desc' === $order ) {
			return $page_no > floor( $db_records / $items_per_page );
		}
		if ( 'asc' === $order ) {
			return $page_no <= ceil( ( $expected_interval_count - $db_records ) / $items_per_page );
		}
		// Invalid ordering.
		return false;
	}

	/**
	 * Updates the LIMIT query part for Intervals query of the report.
	 *
	 * If there are less records in the database than time intervals, then we need to remap offset in SQL query
	 * to fetch correct records.
	 *
	 * @param array  $query_args Query arguments.
	 * @param int    $db_interval_count Database interval count.
	 * @param int    $expected_interval_count Expected interval count on the output.
	 * @param string $table_name Name of the db table relevant for the date constraint.
	 */
	protected function update_intervals_sql_params( &$query_args, $db_interval_count, $expected_interval_count, $table_name ) {
		if ( $db_interval_count === $expected_interval_count ) {
			return;
		}

		$params   = $this->get_limit_params( $query_args );
		$local_tz = new \DateTimeZone( wc_timezone_string() );
		if ( 'date' === strtolower( $query_args['orderby'] ) ) {
			// page X in request translates to slightly different dates in the db, in case some
			// records are missing from the db.
			$start_iteration = 0;
			$end_iteration   = 0;
			if ( 'asc' === strtolower( $query_args['order'] ) ) {
				// ORDER BY date ASC.
				$new_start_date    = $query_args['after'];
				$intervals_to_skip = ( $query_args['page'] - 1 ) * $params['per_page'];
				$latest_end_date   = $query_args['before'];
				for ( $i = 0; $i < $intervals_to_skip; $i++ ) {
					if ( $new_start_date > $latest_end_date ) {
						$new_start_date  = $latest_end_date;
						$start_iteration = 0;
						break;
					}
					$new_start_date = TimeInterval::iterate( $new_start_date, $query_args['interval'] );
					$start_iteration ++;
				}

				$new_end_date = clone $new_start_date;
				for ( $i = 0; $i < $params['per_page']; $i++ ) {
					if ( $new_end_date > $latest_end_date ) {
						break;
					}
					$new_end_date = TimeInterval::iterate( $new_end_date, $query_args['interval'] );
					$end_iteration ++;
				}
				if ( $new_end_date > $latest_end_date ) {
					$new_end_date  = $latest_end_date;
					$end_iteration = 0;
				}
				if ( $end_iteration ) {
					$new_end_date_timestamp = (int) $new_end_date->format( 'U' ) - 1;
					$new_end_date->setTimestamp( $new_end_date_timestamp );
				}
			} else {
				// ORDER BY date DESC.
				$new_end_date        = $query_args['before'];
				$intervals_to_skip   = ( $query_args['page'] - 1 ) * $params['per_page'];
				$earliest_start_date = $query_args['after'];
				for ( $i = 0; $i < $intervals_to_skip; $i++ ) {
					if ( $new_end_date < $earliest_start_date ) {
						$new_end_date  = $earliest_start_date;
						$end_iteration = 0;
						break;
					}
					$new_end_date = TimeInterval::iterate( $new_end_date, $query_args['interval'], true );
					$end_iteration ++;
				}

				$new_start_date = clone $new_end_date;
				for ( $i = 0; $i < $params['per_page']; $i++ ) {
					if ( $new_start_date < $earliest_start_date ) {
						break;
					}
					$new_start_date = TimeInterval::iterate( $new_start_date, $query_args['interval'], true );
					$start_iteration ++;
				}
				if ( $new_start_date < $earliest_start_date ) {
					$new_start_date  = $earliest_start_date;
					$start_iteration = 0;
				}
				if ( $start_iteration ) {
					// @todo Is this correct? should it only be added if iterate runs? other two iterate instances, too?
					$new_start_date_timestamp = (int) $new_start_date->format( 'U' ) + 1;
					$new_start_date->setTimestamp( $new_start_date_timestamp );
				}
			}
			// @todo - Do this without modifying $query_args?
			$query_args['adj_after']  = $new_start_date;
			$query_args['adj_before'] = $new_end_date;
			$adj_after                = $new_start_date->format( TimeInterval::$sql_datetime_format );
			$adj_before               = $new_end_date->format( TimeInterval::$sql_datetime_format );
			$this->interval_query->clear_sql_clause( array( 'where_time', 'limit' ) );
			$this->interval_query->add_sql_clause( 'where_time', "AND {$table_name}.`{$this->date_column_name}` <= '$adj_before'" );
			$this->interval_query->add_sql_clause( 'where_time', "AND {$table_name}.`{$this->date_column_name}` >= '$adj_after'" );
			$this->clear_sql_clause( 'limit' );
			$this->add_sql_clause( 'limit', 'LIMIT 0,' . $params['per_page'] );
		} else {
			if ( 'asc' === $query_args['order'] ) {
				$offset = ( ( $query_args['page'] - 1 ) * $params['per_page'] ) - ( $expected_interval_count - $db_interval_count );
				$offset = $offset < 0 ? 0 : $offset;
				$count  = $query_args['page'] * $params['per_page'] - ( $expected_interval_count - $db_interval_count );
				if ( $count < 0 ) {
					$count = 0;
				} elseif ( $count > $params['per_page'] ) {
					$count = $params['per_page'];
				}

				$this->clear_sql_clause( 'limit' );
				$this->add_sql_clause( 'limit', 'LIMIT ' . $offset . ',' . $count );
			}
			// Otherwise no change in limit clause.
			// @todo - Do this without modifying $query_args?
			$query_args['adj_after']  = $query_args['after'];
			$query_args['adj_before'] = $query_args['before'];
		}
	}

	/**
	 * Casts strings returned from the database to appropriate data types for output.
	 *
	 * @param array $array Associative array of values extracted from the database.
	 * @return array|WP_Error
	 */
	protected function cast_numbers( $array ) {
		$retyped_array = array();
		$column_types  = apply_filters( 'woocommerce_rest_reports_column_types', $this->column_types, $array );
		foreach ( $array as $column_name => $value ) {
			if ( is_array( $value ) ) {
				$value = $this->cast_numbers( $value );
			}

			if ( isset( $column_types[ $column_name ] ) ) {
				$retyped_array[ $column_name ] = $column_types[ $column_name ]( $value );
			} else {
				$retyped_array[ $column_name ] = $value;
			}
		}
		return $retyped_array;
	}

	/**
	 * Returns a list of columns selected by the query_args formatted as a comma separated string.
	 *
	 * @param array $query_args User-supplied options.
	 * @return string
	 */
	protected function selected_columns( $query_args ) {
		$selections = $this->report_columns;

		if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) {
			$keep = array();
			foreach ( $query_args['fields'] as $field ) {
				if ( isset( $selections[ $field ] ) ) {
					$keep[ $field ] = $selections[ $field ];
				}
			}
			$selections = implode( ', ', $keep );
		} else {
			$selections = implode( ', ', $selections );
		}
		return $selections;
	}

	/**
	 * Get the excluded order statuses used when calculating reports.
	 *
	 * @return array
	 */
	protected static function get_excluded_report_order_statuses() {
		$excluded_statuses = \WC_Admin_Settings::get_option( 'woocommerce_excluded_report_order_statuses', array( 'pending', 'failed', 'cancelled' ) );
		$excluded_statuses = array_merge( array( 'auto-draft', 'trash' ), array_map( 'esc_sql', $excluded_statuses ) );
		return apply_filters( 'woocommerce_analytics_excluded_order_statuses', $excluded_statuses );
	}

	/**
	 * Maps order status provided by the user to the one used in the database.
	 *
	 * @param string $status Order status.
	 * @return string
	 */
	protected static function normalize_order_status( $status ) {
		$status = trim( $status );
		return 'wc-' . $status;
	}

	/**
	 * Normalizes order_by clause to match to SQL query.
	 *
	 * @param string $order_by Order by option requested by user.
	 * @return string
	 */
	protected function normalize_order_by( $order_by ) {
		if ( 'date' === $order_by ) {
			return 'time_interval';
		}

		return $order_by;
	}

	/**
	 * Updates start and end dates for intervals so that they represent intervals' borders, not times when data in db were recorded.
	 *
	 * E.g. if there are db records for only Tuesday and Thursday this week, the actual week interval is [Mon, Sun], not [Tue, Thu].
	 *
	 * @param DateTime $start_datetime Start date.
	 * @param DateTime $end_datetime End date.
	 * @param string   $time_interval Time interval, e.g. day, week, month.
	 * @param array    $intervals Array of intervals extracted from SQL db.
	 */
	protected function update_interval_boundary_dates( $start_datetime, $end_datetime, $time_interval, &$intervals ) {
		$local_tz = new \DateTimeZone( wc_timezone_string() );
		foreach ( $intervals as $key => $interval ) {
			$datetime = new \DateTime( $interval['datetime_anchor'], $local_tz );

			$prev_start = TimeInterval::iterate( $datetime, $time_interval, true );
			// @todo Not sure if the +1/-1 here are correct, especially as they are applied before the ?: below.
			$prev_start_timestamp = (int) $prev_start->format( 'U' ) + 1;
			$prev_start->setTimestamp( $prev_start_timestamp );
			if ( $start_datetime ) {
				$date_start                      = $prev_start < $start_datetime ? $start_datetime : $prev_start;
				$intervals[ $key ]['date_start'] = $date_start->format( 'Y-m-d H:i:s' );
			} else {
				$intervals[ $key ]['date_start'] = $prev_start->format( 'Y-m-d H:i:s' );
			}

			$next_end           = TimeInterval::iterate( $datetime, $time_interval );
			$next_end_timestamp = (int) $next_end->format( 'U' ) - 1;
			$next_end->setTimestamp( $next_end_timestamp );
			if ( $end_datetime ) {
				$date_end                      = $next_end > $end_datetime ? $end_datetime : $next_end;
				$intervals[ $key ]['date_end'] = $date_end->format( 'Y-m-d H:i:s' );
			} else {
				$intervals[ $key ]['date_end'] = $next_end->format( 'Y-m-d H:i:s' );
			}

			$intervals[ $key ]['interval'] = $time_interval;
		}
	}

	/**
	 * Change structure of intervals to form a correct response.
	 *
	 * Also converts local datetimes to GMT and adds them to the intervals.
	 *
	 * @param array $intervals Time interval, e.g. day, week, month.
	 */
	protected function create_interval_subtotals( &$intervals ) {
		foreach ( $intervals as $key => $interval ) {
			$start_gmt = TimeInterval::convert_local_datetime_to_gmt( $interval['date_start'] );
			$end_gmt   = TimeInterval::convert_local_datetime_to_gmt( $interval['date_end'] );
			// Move intervals result to subtotals object.
			$intervals[ $key ] = array(
				'interval'       => $interval['time_interval'],
				'date_start'     => $interval['date_start'],
				'date_start_gmt' => $start_gmt->format( TimeInterval::$sql_datetime_format ),
				'date_end'       => $interval['date_end'],
				'date_end_gmt'   => $end_gmt->format( TimeInterval::$sql_datetime_format ),
			);

			unset( $interval['interval'] );
			unset( $interval['date_start'] );
			unset( $interval['date_end'] );
			unset( $interval['datetime_anchor'] );
			unset( $interval['time_interval'] );
			$intervals[ $key ]['subtotals'] = (object) $this->cast_numbers( $interval );
		}
	}

	/**
	 * Fills WHERE clause of SQL request with date-related constraints.
	 *
	 * @param array  $query_args Parameters supplied by the user.
	 * @param string $table_name Name of the db table relevant for the date constraint.
	 */
	protected function add_time_period_sql_params( $query_args, $table_name ) {
		$this->clear_sql_clause( array( 'from', 'where_time', 'where' ) );
		if ( isset( $this->subquery ) ) {
			$this->subquery->clear_sql_clause( 'where_time' );
		}

		if ( isset( $query_args['before'] ) && '' !== $query_args['before'] ) {
			if ( is_a( $query_args['before'], 'WC_DateTime' ) ) {
				$datetime_str = $query_args['before']->date( TimeInterval::$sql_datetime_format );
			} else {
				$datetime_str = $query_args['before']->format( TimeInterval::$sql_datetime_format );
			}
			if ( isset( $this->subquery ) ) {
				$this->subquery->add_sql_clause( 'where_time', "AND {$table_name}.`{$this->date_column_name}` <= '$datetime_str'" );
			} else {
				$this->add_sql_clause( 'where_time', "AND {$table_name}.`{$this->date_column_name}` <= '$datetime_str'" );
			}
		}

		if ( isset( $query_args['after'] ) && '' !== $query_args['after'] ) {
			if ( is_a( $query_args['after'], 'WC_DateTime' ) ) {
				$datetime_str = $query_args['after']->date( TimeInterval::$sql_datetime_format );
			} else {
				$datetime_str = $query_args['after']->format( TimeInterval::$sql_datetime_format );
			}
			if ( isset( $this->subquery ) ) {
				$this->subquery->add_sql_clause( 'where_time', "AND {$table_name}.`{$this->date_column_name}` >= '$datetime_str'" );
			} else {
				$this->add_sql_clause( 'where_time', "AND {$table_name}.`{$this->date_column_name}` >= '$datetime_str'" );
			}
		}
	}

	/**
	 * Fills LIMIT clause of SQL request based on user supplied parameters.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return array
	 */
	protected function get_limit_sql_params( $query_args ) {
		global $wpdb;
		$params = $this->get_limit_params( $query_args );

		$this->clear_sql_clause( 'limit' );
		$this->add_sql_clause( 'limit', $wpdb->prepare( 'LIMIT %d, %d', $params['offset'], $params['per_page'] ) );
		return $params;
	}

	/**
	 * Fills LIMIT parameters of SQL request based on user supplied parameters.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return array
	 */
	protected function get_limit_params( $query_args = array() ) {
		if ( isset( $query_args['per_page'] ) && is_numeric( $query_args['per_page'] ) ) {
			$this->limit_parameters['per_page'] = (int) $query_args['per_page'];
		} else {
			$this->limit_parameters['per_page'] = get_option( 'posts_per_page' );
		}

		$this->limit_parameters['offset'] = 0;
		if ( isset( $query_args['page'] ) ) {
			$this->limit_parameters['offset'] = ( (int) $query_args['page'] - 1 ) * $this->limit_parameters['per_page'];
		}

		return $this->limit_parameters;
	}

	/**
	 * Generates a virtual table given a list of IDs.
	 *
	 * @param array $ids          Array of IDs.
	 * @param array $id_field     Name of the ID field.
	 * @param array $other_values Other values that must be contained in the virtual table.
	 * @return array
	 */
	protected function get_ids_table( $ids, $id_field, $other_values = array() ) {
		global $wpdb;
		$selects = array();
		foreach ( $ids as $id ) {
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$new_select = $wpdb->prepare( "SELECT %s AS {$id_field}", $id );
			foreach ( $other_values as $key => $value ) {
				$new_select .= $wpdb->prepare( ", %s AS {$key}", $value );
			}
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			array_push( $selects, $new_select );
		}
		return join( ' UNION ', $selects );
	}

	/**
	 * Returns a comma separated list of the fields in the `query_args`, if there aren't, returns `report_columns` keys.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return array
	 */
	protected function get_fields( $query_args ) {
		if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) {
			return $query_args['fields'];
		}
		return array_keys( $this->report_columns );
	}

	/**
	 * Returns a comma separated list of the field names prepared to be used for a selection after a join with `default_results`.
	 *
	 * @param array $fields                 Array of fields name.
	 * @param array $default_results_fields Fields to load from `default_results` table.
	 * @param array $outer_selections       Array of fields that are not selected in the inner query.
	 * @return string
	 */
	protected function format_join_selections( $fields, $default_results_fields, $outer_selections = array() ) {
		foreach ( $fields as $i => $field ) {
			foreach ( $default_results_fields as $default_results_field ) {
				if ( $field === $default_results_field ) {
					$field        = esc_sql( $field );
					$fields[ $i ] = "default_results.{$field} AS {$field}";
				}
			}
			if ( in_array( $field, $outer_selections, true ) && array_key_exists( $field, $this->report_columns ) ) {
				$fields[ $i ] = $this->report_columns[ $field ];
			}
		}
		return implode( ', ', $fields );
	}

	/**
	 * Fills ORDER BY clause of SQL request based on user supplied parameters.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 */
	protected function add_order_by_sql_params( $query_args ) {
		if ( isset( $query_args['orderby'] ) ) {
			$order_by_clause = $this->normalize_order_by( esc_sql( $query_args['orderby'] ) );
		} else {
			$order_by_clause = '';
		}

		$this->clear_sql_clause( 'order_by' );
		$this->add_sql_clause( 'order_by', $order_by_clause );
		$this->add_orderby_order_clause( $query_args, $this );
	}

	/**
	 * Fills FROM and WHERE clauses of SQL request for 'Intervals' section of data response based on user supplied parameters.
	 *
	 * @param array  $query_args Parameters supplied by the user.
	 * @param string $table_name Name of the db table relevant for the date constraint.
	 */
	protected function add_intervals_sql_params( $query_args, $table_name ) {
		$this->clear_sql_clause( array( 'from', 'where_time', 'where' ) );

		$this->add_time_period_sql_params( $query_args, $table_name );

		if ( isset( $query_args['interval'] ) && '' !== $query_args['interval'] ) {
			$interval = $query_args['interval'];
			$this->clear_sql_clause( 'select' );
			$this->add_sql_clause( 'select', TimeInterval::db_datetime_format( $interval, $table_name, $this->date_column_name ) );
		}
	}

	/**
	 * Get join and where clauses for refunds based on user supplied parameters.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return array
	 */
	protected function get_refund_subquery( $query_args ) {
		global $wpdb;
		$table_name = $wpdb->prefix . 'wc_order_stats';
		$sql_query  = array(
			'where_clause' => '',
			'from_clause'  => '',
		);

		if ( ! isset( $query_args['refunds'] ) ) {
			return $sql_query;
		}

		if ( 'all' === $query_args['refunds'] ) {
			$sql_query['where_clause'] .= 'parent_id != 0';
		}

		if ( 'none' === $query_args['refunds'] ) {
			$sql_query['where_clause'] .= 'parent_id = 0';
		}

		if ( 'full' === $query_args['refunds'] || 'partial' === $query_args['refunds'] ) {
			$operator                   = 'full' === $query_args['refunds'] ? '=' : '!=';
			$sql_query['from_clause']  .= " JOIN {$table_name} parent_order_stats ON {$table_name}.parent_id = parent_order_stats.order_id";
			$sql_query['where_clause'] .= "parent_order_stats.status {$operator} '{$this->normalize_order_status( 'refunded' )}'";
		}

		return $sql_query;
	}

	/**
	 * Returns an array of products belonging to given categories.
	 *
	 * @param array $categories List of categories IDs.
	 * @return array|stdClass
	 */
	protected function get_products_by_cat_ids( $categories ) {
		$terms = get_terms(
			array(
				'taxonomy' => 'product_cat',
				'include'  => $categories,
			)
		);

		if ( is_wp_error( $terms ) || empty( $terms ) ) {
			return array();
		}

		$args = array(
			'category' => wc_list_pluck( $terms, 'slug' ),
			'limit'    => -1,
			'return'   => 'ids',
		);
		return wc_get_products( $args );
	}

	/**
	 * Get WHERE filter by object ids subquery.
	 *
	 * @param string $select_table Select table name.
	 * @param string $select_field Select table object ID field name.
	 * @param string $filter_table Lookup table name.
	 * @param string $filter_field Lookup table object ID field name.
	 * @param string $compare      Comparison string (IN|NOT IN).
	 * @param string $id_list      Comma separated ID list.
	 *
	 * @return string
	 */
	protected function get_object_where_filter( $select_table, $select_field, $filter_table, $filter_field, $compare, $id_list ) {
		global $wpdb;
		if ( empty( $id_list ) ) {
			return '';
		}

		$lookup_name = isset( $wpdb->$filter_table ) ? $wpdb->$filter_table : $wpdb->prefix . $filter_table;
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return " {$select_table}.{$select_field} {$compare} (
			SELECT
				DISTINCT {$filter_table}.{$select_field}
			FROM
				{$filter_table}
			WHERE
				{$filter_table}.{$filter_field} IN ({$id_list})
		)";
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Returns an array of ids of allowed products, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return array
	 */
	protected function get_included_products_array( $query_args ) {
		$included_products = array();
		$operator          = $this->get_match_operator( $query_args );

		if ( isset( $query_args['category_includes'] ) && is_array( $query_args['category_includes'] ) && count( $query_args['category_includes'] ) > 0 ) {
			$included_products = $this->get_products_by_cat_ids( $query_args['category_includes'] );

			// If no products were found in the specified categories, we will force an empty set
			// by matching a product ID of -1, unless the filters are OR/any and products are specified.
			if ( empty( $included_products ) ) {
				$included_products = array( '-1' );
			}
		}

		if ( isset( $query_args['product_includes'] ) && is_array( $query_args['product_includes'] ) && count( $query_args['product_includes'] ) > 0 ) {
			if ( count( $included_products ) > 0 ) {
				if ( 'AND' === $operator ) {
					// AND results in an intersection between products from selected categories and manually included products.
					$included_products = array_intersect( $included_products, $query_args['product_includes'] );
				} elseif ( 'OR' === $operator ) {
					// OR results in a union of products from selected categories and manually included products.
					$included_products = array_merge( $included_products, $query_args['product_includes'] );
				}
			} else {
				$included_products = $query_args['product_includes'];
			}
		}

		return $included_products;
	}

	/**
	 * Returns comma separated ids of allowed products, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_included_products( $query_args ) {
		$included_products = $this->get_included_products_array( $query_args );
		return implode( ',', $included_products );
	}

	/**
	 * Returns comma separated ids of allowed variations, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_included_variations( $query_args ) {
		return $this->get_filtered_ids( $query_args, 'variation_includes' );
	}

	/**
	 * Returns comma separated ids of excluded variations, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_excluded_variations( $query_args ) {
		return $this->get_filtered_ids( $query_args, 'variation_excludes' );
	}

	/**
	 * Returns an array of ids of disallowed products, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return array
	 */
	protected function get_excluded_products_array( $query_args ) {
		$excluded_products = array();
		$operator          = $this->get_match_operator( $query_args );

		if ( isset( $query_args['category_excludes'] ) && is_array( $query_args['category_excludes'] ) && count( $query_args['category_excludes'] ) > 0 ) {
			$excluded_products = $this->get_products_by_cat_ids( $query_args['category_excludes'] );
		}

		if ( isset( $query_args['product_excludes'] ) && is_array( $query_args['product_excludes'] ) && count( $query_args['product_excludes'] ) > 0 ) {
			$excluded_products = array_merge( $excluded_products, $query_args['product_excludes'] );
		}

		return $excluded_products;
	}

	/**
	 * Returns comma separated ids of excluded products, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_excluded_products( $query_args ) {
		$excluded_products = $this->get_excluded_products_array( $query_args );
		return implode( ',', $excluded_products );
	}

	/**
	 * Returns comma separated ids of included categories, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_included_categories( $query_args ) {
		return $this->get_filtered_ids( $query_args, 'category_includes' );
	}

	/**
	 * Returns comma separated ids of included coupons, based on query arguments from the user.
	 *
	 * @param array  $query_args Parameters supplied by the user.
	 * @param string $field      Field name in the parameter list.
	 * @return string
	 */
	protected function get_included_coupons( $query_args, $field = 'coupon_includes' ) {
		return $this->get_filtered_ids( $query_args, $field );
	}

	/**
	 * Returns comma separated ids of excluded coupons, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_excluded_coupons( $query_args ) {
		return $this->get_filtered_ids( $query_args, 'coupon_excludes' );
	}

	/**
	 * Returns comma separated ids of included orders, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_included_orders( $query_args ) {
		return $this->get_filtered_ids( $query_args, 'order_includes' );
	}

	/**
	 * Returns comma separated ids of excluded orders, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_excluded_orders( $query_args ) {
		return $this->get_filtered_ids( $query_args, 'order_excludes' );
	}

	/**
	 * Returns comma separated ids of included users, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_included_users( $query_args ) {
		return $this->get_filtered_ids( $query_args, 'user_includes' );
	}

	/**
	 * Returns comma separated ids of excluded users, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_excluded_users( $query_args ) {
		return $this->get_filtered_ids( $query_args, 'user_excludes' );
	}

	/**
	 * Returns order status subquery to be used in WHERE SQL query, based on query arguments from the user.
	 *
	 * @param array  $query_args Parameters supplied by the user.
	 * @param string $operator   AND or OR, based on match query argument.
	 * @return string
	 */
	protected function get_status_subquery( $query_args, $operator = 'AND' ) {
		global $wpdb;

		$subqueries        = array();
		$excluded_statuses = array();
		if ( isset( $query_args['status_is'] ) && is_array( $query_args['status_is'] ) && count( $query_args['status_is'] ) > 0 ) {
			$allowed_statuses = array_map( array( $this, 'normalize_order_status' ), esc_sql( $query_args['status_is'] ) );
			if ( $allowed_statuses ) {
				$subqueries[] = "{$wpdb->prefix}wc_order_stats.status IN ( '" . implode( "','", $allowed_statuses ) . "' )";
			}
		}

		if ( isset( $query_args['status_is_not'] ) && is_array( $query_args['status_is_not'] ) && count( $query_args['status_is_not'] ) > 0 ) {
			$excluded_statuses = array_map( array( $this, 'normalize_order_status' ), $query_args['status_is_not'] );
		}

		if ( ( ! isset( $query_args['status_is'] ) || empty( $query_args['status_is'] ) )
			&& ( ! isset( $query_args['status_is_not'] ) || empty( $query_args['status_is_not'] ) )
		) {
			$excluded_statuses = array_map( array( $this, 'normalize_order_status' ), $this->get_excluded_report_order_statuses() );
		}

		if ( $excluded_statuses ) {
			$subqueries[] = "{$wpdb->prefix}wc_order_stats.status NOT IN ( '" . implode( "','", $excluded_statuses ) . "' )";
		}

		return implode( " $operator ", $subqueries );
	}

	/**
	 * Add order status SQL clauses if included in query.
	 *
	 * @param array    $query_args Parameters supplied by the user.
	 * @param string   $table_name Database table name.
	 * @param SqlQuery $sql_query  Query object.
	 */
	protected function add_order_status_clause( $query_args, $table_name, &$sql_query ) {
		global $wpdb;
		$order_status_filter = $this->get_status_subquery( $query_args );
		if ( $order_status_filter ) {
			$sql_query->add_sql_clause( 'join', "JOIN {$wpdb->prefix}wc_order_stats ON {$table_name}.order_id = {$wpdb->prefix}wc_order_stats.order_id" );
			$sql_query->add_sql_clause( 'where', "AND ( {$order_status_filter} )" );
		}
	}

	/**
	 * Add order by SQL clause if included in query.
	 *
	 * @param array    $query_args Parameters supplied by the user.
	 * @param SqlQuery $sql_query  Query object.
	 * @return string Order by clause.
	 */
	protected function add_order_by_clause( $query_args, &$sql_query ) {
		$order_by_clause = '';

		$sql_query->clear_sql_clause( array( 'order_by' ) );
		if ( isset( $query_args['orderby'] ) ) {
			$order_by_clause = $this->normalize_order_by( esc_sql( $query_args['orderby'] ) );
			$sql_query->add_sql_clause( 'order_by', $order_by_clause );
		}

		// Return ORDER BY clause to allow adding the sort field(s) to query via a JOIN.
		return $order_by_clause;
	}

	/**
	 * Add order by order SQL clause.
	 *
	 * @param array    $query_args Parameters supplied by the user.
	 * @param SqlQuery $sql_query  Query object.
	 */
	protected function add_orderby_order_clause( $query_args, &$sql_query ) {
		if ( isset( $query_args['order'] ) ) {
			$sql_query->add_sql_clause( 'order_by', esc_sql( $query_args['order'] ) );
		} else {
			$sql_query->add_sql_clause( 'order_by', 'DESC' );
		}
	}

	/**
	 * Returns customer subquery to be used in WHERE SQL query, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_customer_subquery( $query_args ) {
		global $wpdb;

		$customer_filter = '';
		if ( isset( $query_args['customer_type'] ) ) {
			if ( 'new' === strtolower( $query_args['customer_type'] ) ) {
				$customer_filter = " {$wpdb->prefix}wc_order_stats.returning_customer = 0";
			} elseif ( 'returning' === strtolower( $query_args['customer_type'] ) ) {
				$customer_filter = " {$wpdb->prefix}wc_order_stats.returning_customer = 1";
			}
		}

		return $customer_filter;
	}

	/**
	 * Returns product attribute subquery elements used in JOIN and WHERE clauses,
	 * based on query arguments from the user.
	 *
	 * @param array  $query_args Parameters supplied by the user.
	 * @param string $table_name Database table name.
	 * @return array
	 */
	protected function get_attribute_subqueries( $query_args, $table_name ) {
		global $wpdb;

		$sql_clauses           = array(
			'join'  => array(),
			'where' => array(),
		);
		$match_operator        = $this->get_match_operator( $query_args );
		$post_meta_comparators = array(
			'='  => 'attribute_is',
			'!=' => 'attribute_is_not',
		);

		foreach ( $post_meta_comparators as $comparator => $arg ) {
			if ( ! isset( $query_args[ $arg ] ) || ! is_array( $query_args[ $arg ] ) ) {
				continue;
			}
			foreach ( $query_args[ $arg ] as $attribute_term ) {
				// We expect tuples.
				if ( ! is_array( $attribute_term ) || 2 !== count( $attribute_term ) ) {
					continue;
				}

				// If the tuple is numeric, assume these are IDs.
				if ( is_numeric( $attribute_term[0] ) && is_numeric( $attribute_term[1] ) ) {
					$attribute_id = intval( $attribute_term[0] );
					$term_id      = intval( $attribute_term[1] );

					// Invalid IDs.
					if ( 0 === $attribute_id || 0 === $term_id ) {
						continue;
					}

					// @todo: Use wc_get_attribute () instead ?
					$attr_taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id );
					// Invalid attribute ID.
					if ( empty( $attr_taxonomy ) ) {
						continue;
					}

					$attr_term = get_term_by( 'id', $term_id, $attr_taxonomy );
					// Invalid term ID.
					if ( false === $attr_term ) {
						continue;
					}

					$meta_key   = sanitize_title( $attr_taxonomy );
					$meta_value = $attr_term->slug;
				} else {
					// Assume these are a custom attribute slug/value pair.
					$meta_key   = esc_sql( $attribute_term[0] );
					$meta_value = esc_sql( $attribute_term[1] );
				}

				$join_alias = 'orderitemmeta1';

				if ( empty( $sql_clauses['join'] ) ) {
					$table_name            = esc_sql( $table_name );
					$sql_clauses['join'][] = "JOIN {$wpdb->prefix}woocommerce_order_items orderitems ON orderitems.order_id = {$table_name}.order_id";
				}

				// If we're matching all filters (AND), we'll need multiple JOINs on postmeta.
				// If not, just one.
				if ( 'AND' === $match_operator || 1 === count( $sql_clauses['join'] ) ) {
					$join_idx              = count( $sql_clauses['join'] );
					$join_alias            = 'orderitemmeta' . $join_idx;
					$sql_clauses['join'][] = "JOIN {$wpdb->prefix}woocommerce_order_itemmeta as {$join_alias} ON {$join_alias}.order_item_id = orderitems.order_item_id";
				}

				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$sql_clauses['where'][] = $wpdb->prepare( "( {$join_alias}.meta_key = %s AND {$join_alias}.meta_value {$comparator} %s )", $meta_key, $meta_value );
			}
		}

		// If we're matching multiple attributes and all filters (AND), make sure
		// we're matching attributes on the same product.
		$num_attribute_filters = count( $sql_clauses['join'] );

		for ( $i = 2; $i < $num_attribute_filters; $i++ ) {
			$join_alias            = 'orderitemmeta' . $i;
			$sql_clauses['join'][] = "AND orderitemmeta1.order_item_id = {$join_alias}.order_item_id";
		}

		return $sql_clauses;
	}

	/**
	 * Returns logic operator for WHERE subclause based on 'match' query argument.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_match_operator( $query_args ) {
		$operator = 'AND';

		if ( ! isset( $query_args['match'] ) ) {
			return $operator;
		}

		if ( 'all' === strtolower( $query_args['match'] ) ) {
			$operator = 'AND';
		} elseif ( 'any' === strtolower( $query_args['match'] ) ) {
			$operator = 'OR';
		}
		return $operator;
	}

	/**
	 * Returns filtered comma separated ids, based on query arguments from the user.
	 *
	 * @param array  $query_args Parameters supplied by the user.
	 * @param string $field      Query field to filter.
	 * @param string $separator  Field separator.
	 * @return string
	 */
	protected function get_filtered_ids( $query_args, $field, $separator = ',' ) {
		global $wpdb;

		$ids_str = '';
		$ids     = isset( $query_args[ $field ] ) && is_array( $query_args[ $field ] ) ? $query_args[ $field ] : array();

		/**
		 * Filter the IDs before retrieving report data.
		 *
		 * Allows filtering of the objects included or excluded from reports.
		 *
		 * @param array  $ids        List of object Ids.
		 * @param array  $query_args The original arguments for the request.
		 * @param string $field      The object type.
		 * @param string $context    The data store context.
		 */
		$ids = apply_filters( 'woocommerce_analytics_' . $field, $ids, $query_args, $field, $this->context );

		if ( ! empty( $ids ) ) {
			$placeholders = implode( $separator, array_fill( 0, count( $ids ), '%d' ) );
			/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
			$ids_str = $wpdb->prepare( "{$placeholders}", $ids );
			/* phpcs:enable */
		}
		return $ids_str;
	}

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {}
}
Admin/API/Reports/Orders/Query.php000064400000002350152333011040012707 0ustar00<?php
/**
 * Class for parameter-based Orders Reports querying
 *
 * Example usage:
 * $args = array(
 *          'before'        => '2018-07-19 00:00:00',
 *          'after'         => '2018-07-05 00:00:00',
 *          'interval'      => 'week',
 *          'products'      => array(15, 18),
 *          'coupons'       => array(138),
 *          'status_is'     => array('completed'),
 *          'status_is_not' => array('failed'),
 *          'new_customers' => false,
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Orders;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Orders\Query
 */
class Query extends ReportsQuery {

	/**
	 * Get order data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args       = apply_filters( 'woocommerce_analytics_orders_query_args', $this->get_query_vars() );
		$data_store = \WC_Data_Store::load( 'report-orders' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_orders_select_query', $results, $args );
	}
}
Admin/API/Reports/Orders/Stats/DataStore.php000064400000061766152333011040014606 0ustar00<?php
/**
 * API\Reports\Orders\Stats\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Orders\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
use Automattic\WooCommerce\Utilities\OrderUtil;

/**
 * API\Reports\Orders\Stats\DataStore.
 */
class DataStore extends ReportsDataStore implements DataStoreInterface {

	/**
	 * Table used to get the data.
	 *
	 * @var string
	 */
	protected static $table_name = 'wc_order_stats';

	/**
	 * Cron event name.
	 */
	const CRON_EVENT = 'wc_order_stats_update';

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'orders_stats';

	/**
	 * Type for each column to cast values correctly later.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'orders_count'        => 'intval',
		'num_items_sold'      => 'intval',
		'gross_sales'         => 'floatval',
		'total_sales'         => 'floatval',
		'coupons'             => 'floatval',
		'coupons_count'       => 'intval',
		'refunds'             => 'floatval',
		'taxes'               => 'floatval',
		'shipping'            => 'floatval',
		'net_revenue'         => 'floatval',
		'avg_items_per_order' => 'floatval',
		'avg_order_value'     => 'floatval',
		'total_customers'     => 'intval',
		'products'            => 'intval',
		'segment_id'          => 'intval',
	);

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'orders_stats';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$table_name = self::get_db_table_name();
		// Avoid ambigious columns in SQL query.
		$refunds     = "ABS( SUM( CASE WHEN {$table_name}.net_total < 0 THEN {$table_name}.net_total ELSE 0 END ) )";
		$gross_sales =
			"( SUM({$table_name}.total_sales)" .
			' + COALESCE( SUM(discount_amount), 0 )' . // SUM() all nulls gives null.
			" - SUM({$table_name}.tax_total)" .
			" - SUM({$table_name}.shipping_total)" .
			" + {$refunds}" .
			' ) as gross_sales';

		$this->report_columns = array(
			'orders_count'        => "SUM( CASE WHEN {$table_name}.parent_id = 0 THEN 1 ELSE 0 END ) as orders_count",
			'num_items_sold'      => "SUM({$table_name}.num_items_sold) as num_items_sold",
			'gross_sales'         => $gross_sales,
			'total_sales'         => "SUM({$table_name}.total_sales) AS total_sales",
			'coupons'             => 'COALESCE( SUM(discount_amount), 0 ) AS coupons', // SUM() all nulls gives null.
			'coupons_count'       => 'COALESCE( coupons_count, 0 ) as coupons_count',
			'refunds'             => "{$refunds} AS refunds",
			'taxes'               => "SUM({$table_name}.tax_total) AS taxes",
			'shipping'            => "SUM({$table_name}.shipping_total) AS shipping",
			'net_revenue'         => "SUM({$table_name}.net_total) AS net_revenue",
			'avg_items_per_order' => "SUM( {$table_name}.num_items_sold ) / SUM( CASE WHEN {$table_name}.parent_id = 0 THEN 1 ELSE 0 END ) AS avg_items_per_order",
			'avg_order_value'     => "SUM( {$table_name}.net_total ) / SUM( CASE WHEN {$table_name}.parent_id = 0 THEN 1 ELSE 0 END ) AS avg_order_value",
			'total_customers'     => "COUNT( DISTINCT( {$table_name}.customer_id ) ) as total_customers",
		);
	}

	/**
	 * Set up all the hooks for maintaining and populating table data.
	 */
	public static function init() {
		add_action( 'delete_post', array( __CLASS__, 'delete_order' ) );
	}

	/**
	 * Updates the totals and intervals database queries with parameters used for Orders report: categories, coupons and order status.
	 *
	 * @param array $query_args      Query arguments supplied by the user.
	 */
	protected function orders_stats_sql_filter( $query_args ) {
		// phpcs:ignore Generic.Commenting.Todo.TaskFound
		// @todo Performance of all of this?
		global $wpdb;

		$from_clause        = '';
		$orders_stats_table = self::get_db_table_name();
		$product_lookup     = $wpdb->prefix . 'wc_order_product_lookup';
		$coupon_lookup      = $wpdb->prefix . 'wc_order_coupon_lookup';
		$tax_rate_lookup    = $wpdb->prefix . 'wc_order_tax_lookup';
		$operator           = $this->get_match_operator( $query_args );

		$where_filters = array();

		// Products filters.
		$where_filters[] = $this->get_object_where_filter(
			$orders_stats_table,
			'order_id',
			$product_lookup,
			'product_id',
			'IN',
			$this->get_included_products( $query_args )
		);
		$where_filters[] = $this->get_object_where_filter(
			$orders_stats_table,
			'order_id',
			$product_lookup,
			'product_id',
			'NOT IN',
			$this->get_excluded_products( $query_args )
		);

		// Variations filters.
		$where_filters[] = $this->get_object_where_filter(
			$orders_stats_table,
			'order_id',
			$product_lookup,
			'variation_id',
			'IN',
			$this->get_included_variations( $query_args )
		);
		$where_filters[] = $this->get_object_where_filter(
			$orders_stats_table,
			'order_id',
			$product_lookup,
			'variation_id',
			'NOT IN',
			$this->get_excluded_variations( $query_args )
		);

		// Coupons filters.
		$where_filters[] = $this->get_object_where_filter(
			$orders_stats_table,
			'order_id',
			$coupon_lookup,
			'coupon_id',
			'IN',
			$this->get_included_coupons( $query_args )
		);
		$where_filters[] = $this->get_object_where_filter(
			$orders_stats_table,
			'order_id',
			$coupon_lookup,
			'coupon_id',
			'NOT IN',
			$this->get_excluded_coupons( $query_args )
		);

		// Tax rate filters.
		$where_filters[] = $this->get_object_where_filter(
			$orders_stats_table,
			'order_id',
			$tax_rate_lookup,
			'tax_rate_id',
			'IN',
			implode( ',', $query_args['tax_rate_includes'] )
		);
		$where_filters[] = $this->get_object_where_filter(
			$orders_stats_table,
			'order_id',
			$tax_rate_lookup,
			'tax_rate_id',
			'NOT IN',
			implode( ',', $query_args['tax_rate_excludes'] )
		);

		// Product attribute filters.
		$attribute_subqueries = $this->get_attribute_subqueries( $query_args, $orders_stats_table );
		if ( $attribute_subqueries['join'] && $attribute_subqueries['where'] ) {
			// Build a subquery for getting order IDs by product attribute(s).
			// Done here since our use case is a little more complicated than get_object_where_filter() can handle.
			$attribute_subquery = new SqlQuery();
			$attribute_subquery->add_sql_clause( 'select', "{$orders_stats_table}.order_id" );
			$attribute_subquery->add_sql_clause( 'from', $orders_stats_table );

			// JOIN on product lookup.
			$attribute_subquery->add_sql_clause( 'join', "JOIN {$product_lookup} ON {$orders_stats_table}.order_id = {$product_lookup}.order_id" );

			// Add JOINs for matching attributes.
			foreach ( $attribute_subqueries['join'] as $attribute_join ) {
				$attribute_subquery->add_sql_clause( 'join', $attribute_join );
			}
			// Add WHEREs for matching attributes.
			$attribute_subquery->add_sql_clause( 'where', 'AND (' . implode( " {$operator} ", $attribute_subqueries['where'] ) . ')' );

			// Generate subquery statement and add to our where filters.
			$where_filters[] = "{$orders_stats_table}.order_id IN (" . $attribute_subquery->get_query_statement() . ')';
		}

		$where_filters[] = $this->get_customer_subquery( $query_args );
		$refund_subquery = $this->get_refund_subquery( $query_args );
		$from_clause    .= $refund_subquery['from_clause'];
		if ( $refund_subquery['where_clause'] ) {
			$where_filters[] = $refund_subquery['where_clause'];
		}

		$where_filters   = array_filter( $where_filters );
		$where_subclause = implode( " $operator ", $where_filters );

		// Append status filter after to avoid matching ANY on default statuses.
		$order_status_filter = $this->get_status_subquery( $query_args, $operator );
		if ( $order_status_filter ) {
			if ( empty( $query_args['status_is'] ) && empty( $query_args['status_is_not'] ) ) {
				$operator = 'AND';
			}
			$where_subclause = implode( " $operator ", array_filter( array( $where_subclause, $order_status_filter ) ) );
		}

		// To avoid requesting the subqueries twice, the result is applied to all queries passed to the method.
		if ( $where_subclause ) {
			$this->total_query->add_sql_clause( 'where', "AND ( $where_subclause )" );
			$this->total_query->add_sql_clause( 'join', $from_clause );
			$this->interval_query->add_sql_clause( 'where', "AND ( $where_subclause )" );
			$this->interval_query->add_sql_clause( 'join', $from_clause );
		}
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only applied when not using REST API, as the API has its own defaults that overwrite these for most values (except before, after, etc).
		$defaults   = array(
			'per_page'          => get_option( 'posts_per_page' ),
			'page'              => 1,
			'order'             => 'DESC',
			'orderby'           => 'date',
			'before'            => TimeInterval::default_before(),
			'after'             => TimeInterval::default_after(),
			'interval'          => 'week',
			'fields'            => '*',
			'segmentby'         => '',

			'match'             => 'all',
			'status_is'         => array(),
			'status_is_not'     => array(),
			'product_includes'  => array(),
			'product_excludes'  => array(),
			'coupon_includes'   => array(),
			'coupon_excludes'   => array(),
			'tax_rate_includes' => array(),
			'tax_rate_excludes' => array(),
			'customer_type'     => '',
			'category_includes' => array(),
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$data = (object) array(
				'totals'    => (object) array(),
				'intervals' => (object) array(),
				'total'     => 0,
				'pages'     => 0,
				'page_no'   => 0,
			);

			$selections = $this->selected_columns( $query_args );
			$this->add_time_period_sql_params( $query_args, $table_name );
			$this->add_intervals_sql_params( $query_args, $table_name );
			$this->add_order_by_sql_params( $query_args );
			$where_time  = $this->get_sql_clause( 'where_time' );
			$params      = $this->get_limit_sql_params( $query_args );
			$coupon_join = "LEFT JOIN (
						SELECT
							order_id,
							SUM(discount_amount) AS discount_amount,
							COUNT(DISTINCT coupon_id) AS coupons_count
						FROM
							{$wpdb->prefix}wc_order_coupon_lookup
						GROUP BY
							order_id
						) order_coupon_lookup
						ON order_coupon_lookup.order_id = {$wpdb->prefix}wc_order_stats.order_id";

			// Additional filtering for Orders report.
			$this->orders_stats_sql_filter( $query_args );
			$this->total_query->add_sql_clause( 'select', $selections );
			$this->total_query->add_sql_clause( 'left_join', $coupon_join );
			$this->total_query->add_sql_clause( 'where_time', $where_time );
			$totals = $wpdb->get_results(
				$this->total_query->get_query_statement(),
				ARRAY_A
			); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
			if ( null === $totals ) {
				return new \WP_Error( 'woocommerce_analytics_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
			}

			// phpcs:ignore Generic.Commenting.Todo.TaskFound
			// @todo Remove these assignements when refactoring segmenter classes to use query objects.
			$totals_query    = array(
				'from_clause'       => $this->total_query->get_sql_clause( 'join' ),
				'where_time_clause' => $where_time,
				'where_clause'      => $this->total_query->get_sql_clause( 'where' ),
			);
			$intervals_query = array(
				'select_clause'     => $this->get_sql_clause( 'select' ),
				'from_clause'       => $this->interval_query->get_sql_clause( 'join' ),
				'where_time_clause' => $where_time,
				'where_clause'      => $this->interval_query->get_sql_clause( 'where' ),
				'limit'             => $this->get_sql_clause( 'limit' ),
			);

			$unique_products            = $this->get_unique_product_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] );
			$totals[0]['products']      = $unique_products;
			$segmenter                  = new Segmenter( $query_args, $this->report_columns );
			$unique_coupons             = $this->get_unique_coupon_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] );
			$totals[0]['coupons_count'] = $unique_coupons;
			$totals[0]['segments']      = $segmenter->get_totals_segments( $totals_query, $table_name );
			$totals                     = (object) $this->cast_numbers( $totals[0] );

			$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
			$this->interval_query->add_sql_clause( 'left_join', $coupon_join );
			$this->interval_query->add_sql_clause( 'where_time', $where_time );
			$db_intervals = $wpdb->get_col(
				$this->interval_query->get_query_statement()
			); // phpcs:ignore cache ok, DB call ok, , unprepared SQL ok.

			$db_interval_count       = count( $db_intervals );
			$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
			$total_pages             = (int) ceil( $expected_interval_count / $params['per_page'] );

			if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
				return $data;
			}

			$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
			$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
			$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
			$this->interval_query->add_sql_clause( 'select', ", MAX(${table_name}.date_created) AS datetime_anchor" );
			if ( '' !== $selections ) {
				$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
			}
			$intervals = $wpdb->get_results(
				$this->interval_query->get_query_statement(),
				ARRAY_A
			); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.

			if ( null === $intervals ) {
				return new \WP_Error( 'woocommerce_analytics_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
			}

			if ( isset( $intervals[0] ) ) {
				$unique_coupons                = $this->get_unique_coupon_count( $intervals_query['from_clause'], $intervals_query['where_time_clause'], $intervals_query['where_clause'], true );
				$intervals[0]['coupons_count'] = $unique_coupons;
			}

			$data = (object) array(
				'totals'    => $totals,
				'intervals' => $intervals,
				'total'     => $expected_interval_count,
				'pages'     => $total_pages,
				'page_no'   => (int) $query_args['page'],
			);

			if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
				$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
				$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
				$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
			} else {
				$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
			}
			$segmenter->add_intervals_segments( $data, $intervals_query, $table_name );
			$this->create_interval_subtotals( $data->intervals );

			$this->set_cached_data( $cache_key, $data );
		}

		return $data;
	}

	/**
	 * Get unique products based on user time query
	 *
	 * @param string $from_clause       From clause with date query.
	 * @param string $where_time_clause Where clause with date query.
	 * @param string $where_clause      Where clause with date query.
	 * @return integer Unique product count.
	 */
	public function get_unique_product_count( $from_clause, $where_time_clause, $where_clause ) {
		global $wpdb;

		$table_name = self::get_db_table_name();
		return $wpdb->get_var(
			"SELECT
					COUNT( DISTINCT {$wpdb->prefix}wc_order_product_lookup.product_id )
				FROM
					{$wpdb->prefix}wc_order_product_lookup JOIN {$table_name} ON {$wpdb->prefix}wc_order_product_lookup.order_id = {$table_name}.order_id
					{$from_clause}
				WHERE
					1=1
					{$where_time_clause}
					{$where_clause}"
		); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
	}

	/**
	 * Get unique coupons based on user time query
	 *
	 * @param string $from_clause       From clause with date query.
	 * @param string $where_time_clause Where clause with date query.
	 * @param string $where_clause      Where clause with date query.
	 * @return integer Unique product count.
	 */
	public function get_unique_coupon_count( $from_clause, $where_time_clause, $where_clause ) {
		global $wpdb;

		$table_name = self::get_db_table_name();
		return $wpdb->get_var(
			"SELECT
					COUNT(DISTINCT coupon_id)
				FROM
					{$wpdb->prefix}wc_order_coupon_lookup JOIN {$table_name} ON {$wpdb->prefix}wc_order_coupon_lookup.order_id = {$table_name}.order_id
					{$from_clause}
				WHERE
					1=1
					{$where_time_clause}
					{$where_clause}"
		); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
	}

	/**
	 * Add order information to the lookup table when orders are created or modified.
	 *
	 * @param int $post_id Post ID.
	 * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
	 */
	public static function sync_order( $post_id ) {
		if ( ! OrderUtil::is_order( $post_id, array( 'shop_order', 'shop_order_refund' ) ) ) {
			return -1;
		}

		$order = wc_get_order( $post_id );
		if ( ! $order ) {
			return -1;
		}

		return self::update( $order );
	}

	/**
	 * Update the database with stats data.
	 *
	 * @param WC_Order|WC_Order_Refund $order Order or refund to update row for.
	 * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
	 */
	public static function update( $order ) {
		global $wpdb;
		$table_name = self::get_db_table_name();

		if ( ! $order->get_id() || ! $order->get_date_created() ) {
			return -1;
		}

		/**
		 * Filters order stats data.
		 *
		 * @param array $data Data written to order stats lookup table.
		 * @param WC_Order $order  Order object.
		 *
		 * @since 4.0.0
		 */
		$data = apply_filters(
			'woocommerce_analytics_update_order_stats_data',
			array(
				'order_id'           => $order->get_id(),
				'parent_id'          => $order->get_parent_id(),
				'date_created'       => $order->get_date_created()->date( 'Y-m-d H:i:s' ),
				'date_created_gmt'   => gmdate( 'Y-m-d H:i:s', $order->get_date_created()->getTimestamp() ),
				'num_items_sold'     => self::get_num_items_sold( $order ),
				'total_sales'        => $order->get_total(),
				'tax_total'          => $order->get_total_tax(),
				'shipping_total'     => $order->get_shipping_total(),
				'net_total'          => self::get_net_total( $order ),
				'status'             => self::normalize_order_status( $order->get_status() ),
				'customer_id'        => $order->get_report_customer_id(),
				'returning_customer' => $order->is_returning_customer(),
			),
			$order
		);

		$format = array(
			'%d',
			'%d',
			'%s',
			'%s',
			'%d',
			'%f',
			'%f',
			'%f',
			'%f',
			'%s',
			'%d',
			'%d',
		);

		if ( 'shop_order_refund' === $order->get_type() ) {
			$parent_order = wc_get_order( $order->get_parent_id() );
			if ( $parent_order ) {
				$data['parent_id'] = $parent_order->get_id();
				$data['status']    = self::normalize_order_status( $parent_order->get_status() );
			}
		}

		// Update or add the information to the DB.
		$result = $wpdb->replace( $table_name, $data, $format );

		/**
		 * Fires when order's stats reports are updated.
		 *
		 * @param int $order_id Order ID.
		 *
		 * @since 4.0.0.
		 */
		do_action( 'woocommerce_analytics_update_order_stats', $order->get_id() );

		// Check the rows affected for success. Using REPLACE can affect 2 rows if the row already exists.
		return ( 1 === $result || 2 === $result );
	}

	/**
	 * Deletes the order stats when an order is deleted.
	 *
	 * @param int $post_id Post ID.
	 */
	public static function delete_order( $post_id ) {
		global $wpdb;
		$order_id = (int) $post_id;

		if ( ! OrderUtil::is_order( $post_id, array( 'shop_order', 'shop_order_refund' ) ) ) {
			return;
		}

		// Retrieve customer details before the order is deleted.
		$order       = wc_get_order( $order_id );
		$customer_id = absint( CustomersDataStore::get_existing_customer_id_from_order( $order ) );

		// Delete the order.
		$wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) );
		/**
		 * Fires when orders stats are deleted.
		 *
		 * @param int $order_id Order ID.
		 * @param int $customer_id Customer ID.
		 *
		 * @since 4.0.0
		 */
		do_action( 'woocommerce_analytics_delete_order_stats', $order_id, $customer_id );

		ReportsCache::invalidate();
	}


	/**
	 * Calculation methods.
	 */

	/**
	 * Get number of items sold among all orders.
	 *
	 * @param array $order WC_Order object.
	 * @return int
	 */
	protected static function get_num_items_sold( $order ) {
		$num_items = 0;

		$line_items = $order->get_items( 'line_item' );
		foreach ( $line_items as $line_item ) {
			$num_items += $line_item->get_quantity();
		}

		return $num_items;
	}

	/**
	 * Get the net amount from an order without shipping, tax, or refunds.
	 *
	 * @param array $order WC_Order object.
	 * @return float
	 */
	protected static function get_net_total( $order ) {
		$net_total = floatval( $order->get_total() ) - floatval( $order->get_total_tax() ) - floatval( $order->get_shipping_total() );
		return (float) $net_total;
	}

	/**
	 * Check to see if an order's customer has made previous orders or not
	 *
	 * @param array     $order WC_Order object.
	 * @param int|false $customer_id Customer ID. Optional.
	 * @return bool
	 */
	public static function is_returning_customer( $order, $customer_id = null ) {
		if ( is_null( $customer_id ) ) {
			$customer_id = \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore::get_existing_customer_id_from_order( $order );
		}

		if ( ! $customer_id ) {
			return false;
		}

		$oldest_orders = \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore::get_oldest_orders( $customer_id );

		if ( empty( $oldest_orders ) ) {
			return false;
		}

		$first_order       = $oldest_orders[0];
		$second_order      = isset( $oldest_orders[1] ) ? $oldest_orders[1] : false;
		$excluded_statuses = self::get_excluded_report_order_statuses();

		// Order is older than previous first order.
		if ( $order->get_date_created() < wc_string_to_datetime( $first_order->date_created ) &&
			! in_array( $order->get_status(), $excluded_statuses, true )
		) {
			self::set_customer_first_order( $customer_id, $order->get_id() );
			return false;
		}

		// The current order is the oldest known order.
		$is_first_order = (int) $order->get_id() === (int) $first_order->order_id;
		// Order date has changed and next oldest is now the first order.
		$date_change = $second_order &&
			$order->get_date_created() > wc_string_to_datetime( $first_order->date_created ) &&
			wc_string_to_datetime( $second_order->date_created ) < $order->get_date_created();
		// Status has changed to an excluded status and next oldest order is now the first order.
		$status_change = $second_order &&
			in_array( $order->get_status(), $excluded_statuses, true );
		if ( $is_first_order && ( $date_change || $status_change ) ) {
			self::set_customer_first_order( $customer_id, $second_order->order_id );
			return true;
		}

		return (int) $order->get_id() !== (int) $first_order->order_id;
	}

	/**
	 * Set a customer's first order and all others to returning.
	 *
	 * @param int $customer_id Customer ID.
	 * @param int $order_id Order ID.
	 */
	protected static function set_customer_first_order( $customer_id, $order_id ) {
		global $wpdb;
		$orders_stats_table = self::get_db_table_name();

		$wpdb->query(
			$wpdb->prepare(
				"UPDATE ${orders_stats_table} SET returning_customer = CASE WHEN order_id = %d THEN false ELSE true END WHERE customer_id = %d",
				$order_id,
				$customer_id
			)
		);
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		unset( $this->subquery );
		$this->total_query = new SqlQuery( $this->context . '_total' );
		$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );

		$this->interval_query = new SqlQuery( $this->context . '_interval' );
		$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
		$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
	}
}
Admin/API/Reports/Orders/Stats/Query.php000064400000003004152333011040014002 0ustar00<?php
/**
 * Class for parameter-based Order Stats Reports querying
 *
 * Example usage:
 * $args = array(
 *          'before'       => '2018-07-19 00:00:00',
 *          'after'        => '2018-07-05 00:00:00',
 *          'interval'     => 'week',
 *          'categories'   => array(15, 18),
 *          'coupons'      => array(138),
 *          'status_in'    => array('completed'),
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Orders\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Orders\Stats\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Orders report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array(
			'fields' => array(
				'net_revenue',
				'avg_order_value',
				'orders_count',
				'avg_items_per_order',
				'num_items_sold',
				'coupons',
				'coupons_count',
				'total_customers',
			),
		);
	}

	/**
	 * Get revenue data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_orders_stats_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-orders-stats' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_orders_stats_select_query', $results, $args );
	}
}
Admin/API/Reports/Orders/Stats/Segmenter.php000064400000051635152333011040014643 0ustar00<?php
/**
 * Class for adding segmenting support without cluttering the data stores.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Orders\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Segmenter as ReportsSegmenter;
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;

/**
 * Date & time interval and numeric range handling class for Reporting API.
 */
class Segmenter extends ReportsSegmenter {

	/**
	 * Returns column => query mapping to be used for product-related product-level segmenting query
	 * (e.g. products sold, revenue from product X when segmenting by category).
	 *
	 * @param string $products_table Name of SQL table containing the product-level segmenting info.
	 *
	 * @return array Column => SELECT query mapping.
	 */
	protected function get_segment_selections_product_level( $products_table ) {
		$columns_mapping = array(
			'num_items_sold' => "SUM($products_table.product_qty) as num_items_sold",
			'total_sales'    => "SUM($products_table.product_gross_revenue) AS total_sales",
			'coupons'        => 'SUM( coupon_lookup_left_join.discount_amount ) AS coupons',
			'coupons_count'  => 'COUNT( DISTINCT( coupon_lookup_left_join.coupon_id ) ) AS coupons_count',
			'refunds'        => "SUM( CASE WHEN $products_table.product_gross_revenue < 0 THEN $products_table.product_gross_revenue ELSE 0 END ) AS refunds",
			'taxes'          => "SUM($products_table.tax_amount) AS taxes",
			'shipping'       => "SUM($products_table.shipping_amount) AS shipping",
			'net_revenue'    => "SUM($products_table.product_net_revenue) AS net_revenue",
		);

		return $columns_mapping;
	}

	/**
	 * Returns column => query mapping to be used for order-related product-level segmenting query
	 * (e.g. avg items per order when segmented by category).
	 *
	 * @param string $unique_orders_table Name of SQL table containing the order-level segmenting info.
	 *
	 * @return array Column => SELECT query mapping.
	 */
	protected function get_segment_selections_order_level( $unique_orders_table ) {
		$columns_mapping = array(
			'orders_count'        => "COUNT($unique_orders_table.order_id) AS orders_count",
			'avg_items_per_order' => "AVG($unique_orders_table.num_items_sold) AS avg_items_per_order",
			'avg_order_value'     => "SUM($unique_orders_table.net_total) / COUNT($unique_orders_table.order_id) AS avg_order_value",
			'total_customers'     => "COUNT( DISTINCT( $unique_orders_table.customer_id ) ) AS total_customers",
		);

		return $columns_mapping;
	}

	/**
	 * Returns column => query mapping to be used for order-level segmenting query
	 * (e.g. avg items per order or Net sales when segmented by coupons).
	 *
	 * @param string $order_stats_table Name of SQL table containing the order-level info.
	 * @param array  $overrides Array of overrides for default column calculations.
	 *
	 * @return array Column => SELECT query mapping.
	 */
	protected function segment_selections_orders( $order_stats_table, $overrides = array() ) {
		$columns_mapping = array(
			'num_items_sold'      => "SUM($order_stats_table.num_items_sold) as num_items_sold",
			'total_sales'         => "SUM($order_stats_table.total_sales) AS total_sales",
			'coupons'             => "SUM($order_stats_table.discount_amount) AS coupons",
			'coupons_count'       => 'COUNT( DISTINCT(coupon_lookup_left_join.coupon_id) ) AS coupons_count',
			'refunds'             => "SUM( CASE WHEN $order_stats_table.parent_id != 0 THEN $order_stats_table.total_sales END ) AS refunds",
			'taxes'               => "SUM($order_stats_table.tax_total) AS taxes",
			'shipping'            => "SUM($order_stats_table.shipping_total) AS shipping",
			'net_revenue'         => "SUM($order_stats_table.net_total) AS net_revenue",
			'orders_count'        => "COUNT($order_stats_table.order_id) AS orders_count",
			'avg_items_per_order' => "AVG($order_stats_table.num_items_sold) AS avg_items_per_order",
			'avg_order_value'     => "SUM($order_stats_table.net_total) / COUNT($order_stats_table.order_id) AS avg_order_value",
			'total_customers'     => "COUNT( DISTINCT( $order_stats_table.customer_id ) ) AS total_customers",
		);

		if ( $overrides ) {
			$columns_mapping = array_merge( $columns_mapping, $overrides );
		}

		return $columns_mapping;
	}

	/**
	 * Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
	 *
	 * @param array  $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $segmenting_dimension_name Name of the segmenting dimension.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $totals_query Array of SQL clauses for totals query.
	 * @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
	 *
	 * @return array
	 */
	protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) {
		global $wpdb;

		$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';

		// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
		// Product-level numbers.
		$segments_products = $wpdb->get_results(
			"SELECT
						$segmenting_groupby AS $segmenting_dimension_name
						{$segmenting_selections['product_level']}
					FROM
						$table_name
						$segmenting_from
						{$totals_query['from_clause']}
					WHERE
						1=1
						{$totals_query['where_time_clause']}
						{$totals_query['where_clause']}
						$segmenting_where
					GROUP BY
						$segmenting_groupby",
			ARRAY_A
		); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.

		// Order level numbers.
		// As there can be 2 same product ids (or variation ids) per one order, the orders first need to be uniqued before calculating averages, customer counts, etc.
		$segments_orders = $wpdb->get_results(
			"SELECT
				    $unique_orders_table.$segmenting_dimension_name AS $segmenting_dimension_name
				    {$segmenting_selections['order_level']}
				FROM
				(
					SELECT
				        $table_name.order_id,
				        $segmenting_groupby AS $segmenting_dimension_name,
				        MAX( num_items_sold ) AS num_items_sold,
				        MAX( net_total ) as net_total,
				        MAX( returning_customer ) AS returning_customer,
						MAX( $table_name.customer_id ) as customer_id
				    FROM
				        $table_name
				        $segmenting_from
				        {$totals_query['from_clause']}
				    WHERE
				        1=1
						{$totals_query['where_time_clause']}
						{$totals_query['where_clause']}
						$segmenting_where
				    GROUP BY
				        $product_segmenting_table.order_id, $segmenting_groupby
				) AS $unique_orders_table
				GROUP BY
				    $unique_orders_table.$segmenting_dimension_name",
			ARRAY_A
		); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.

		$totals_segments = $this->merge_segment_totals_results( $segmenting_dimension_name, $segments_products, $segments_orders );
		return $totals_segments;
	}

	/**
	 * Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
	 *
	 * @param array  $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $segmenting_dimension_name Name of the segmenting dimension.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $intervals_query Array of SQL clauses for intervals query.
	 * @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
	 *
	 * @return array
	 */
	protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) {
		global $wpdb;

		$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';

		// LIMIT offset, rowcount needs to be updated to LIMIT offset, rowcount * max number of segments.
		$limit_parts      = explode( ',', $intervals_query['limit'] );
		$orig_rowcount    = intval( $limit_parts[1] );
		$segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() );

		// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
		// Product-level numbers.
		$segments_products = $wpdb->get_results(
			"SELECT
						{$intervals_query['select_clause']} AS time_interval,
						$segmenting_groupby AS $segmenting_dimension_name
						{$segmenting_selections['product_level']}
					FROM
						$table_name
						$segmenting_from
						{$intervals_query['from_clause']}
					WHERE
						1=1
						{$intervals_query['where_time_clause']}
						{$intervals_query['where_clause']}
						$segmenting_where
					GROUP BY
						time_interval, $segmenting_groupby
					$segmenting_limit",
			ARRAY_A
		); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.

		// Order level numbers.
		// As there can be 2 same product ids (or variation ids) per one order, the orders first need to be uniqued before calculating averages, customer counts, etc.
		$segments_orders = $wpdb->get_results(
			"SELECT
					$unique_orders_table.time_interval AS time_interval,
				    $unique_orders_table.$segmenting_dimension_name AS $segmenting_dimension_name
				    {$segmenting_selections['order_level']}
				FROM
				(
					SELECT
						MAX( $table_name.date_created ) AS datetime_anchor,
						{$intervals_query['select_clause']} AS time_interval,
				        $table_name.order_id,
				        $segmenting_groupby AS $segmenting_dimension_name,
				        MAX( num_items_sold ) AS num_items_sold,
				        MAX( net_total ) as net_total,
				        MAX( returning_customer ) AS returning_customer,
						MAX( $table_name.customer_id ) as customer_id
				    FROM
				        $table_name
				        $segmenting_from
				        {$intervals_query['from_clause']}
				    WHERE
				        1=1
						{$intervals_query['where_time_clause']}
						{$intervals_query['where_clause']}
						$segmenting_where
				    GROUP BY
				        time_interval, $product_segmenting_table.order_id, $segmenting_groupby
				) AS $unique_orders_table
				GROUP BY
				    time_interval, $unique_orders_table.$segmenting_dimension_name
				$segmenting_limit",
			ARRAY_A
		); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.

		$intervals_segments = $this->merge_segment_intervals_results( $segmenting_dimension_name, $segments_products, $segments_orders );
		return $intervals_segments;
	}

	/**
	 * Calculate segments for totals query where the segmenting property is bound to order (e.g. coupon or customer type).
	 *
	 * @param string $segmenting_select SELECT part of segmenting SQL query.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $totals_query Array of SQL clauses for intervals query.
	 *
	 * @return array
	 */
	protected function get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $totals_query ) {
		global $wpdb;

		$totals_segments = $wpdb->get_results(
			"SELECT
						$segmenting_groupby
						$segmenting_select
					FROM
						$table_name
						$segmenting_from
						{$totals_query['from_clause']}
					WHERE
						1=1
						{$totals_query['where_time_clause']}
						{$totals_query['where_clause']}
						$segmenting_where
					GROUP BY
						$segmenting_groupby",
			ARRAY_A
		); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.

		// Reformat result.
		$totals_segments = $this->reformat_totals_segments( $totals_segments, $segmenting_groupby );
		return $totals_segments;
	}

	/**
	 * Calculate segments for intervals query where the segmenting property is bound to order (e.g. coupon or customer type).
	 *
	 * @param string $segmenting_select SELECT part of segmenting SQL query.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $intervals_query Array of SQL clauses for intervals query.
	 *
	 * @return array
	 */
	protected function get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $intervals_query ) {
		global $wpdb;
		$segmenting_limit = '';
		$limit_parts      = explode( ',', $intervals_query['limit'] );
		if ( 2 === count( $limit_parts ) ) {
			$orig_rowcount    = intval( $limit_parts[1] );
			$segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() );
		}

		$intervals_segments = $wpdb->get_results(
			"SELECT
						MAX($table_name.date_created) AS datetime_anchor,
						{$intervals_query['select_clause']} AS time_interval,
						$segmenting_groupby
						$segmenting_select
					FROM
						$table_name
						$segmenting_from
						{$intervals_query['from_clause']}
					WHERE
						1=1
						{$intervals_query['where_time_clause']}
						{$intervals_query['where_clause']}
						$segmenting_where
					GROUP BY
						time_interval, $segmenting_groupby
					$segmenting_limit",
			ARRAY_A
		); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.

		// Reformat result.
		$intervals_segments = $this->reformat_intervals_segments( $intervals_segments, $segmenting_groupby );
		return $intervals_segments;
	}

	/**
	 * Return array of segments formatted for REST response.
	 *
	 * @param string $type Type of segments to return--'totals' or 'intervals'.
	 * @param array  $query_params SQL query parameter array.
	 * @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
	 *
	 * @return array
	 * @throws \Automattic\WooCommerce\Admin\API\Reports\ParameterException In case of segmenting by variations, when no parent product is specified.
	 */
	protected function get_segments( $type, $query_params, $table_name ) {
		global $wpdb;
		if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
			return array();
		}

		$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
		$unique_orders_table      = 'uniq_orders';
		$segmenting_from          = "LEFT JOIN {$wpdb->prefix}wc_order_coupon_lookup AS coupon_lookup_left_join ON ($table_name.order_id = coupon_lookup_left_join.order_id) ";
		$segmenting_where         = '';

		// Product, variation, and category are bound to product, so here product segmenting table is required,
		// while coupon and customer are bound to order, so we don't need the extra JOIN for those.
		// This also means that segment selections need to be calculated differently.
		if ( 'product' === $this->query_args['segmentby'] ) {
			// @todo How to handle shipping taxes when grouped by product?
			$product_level_columns     = $this->get_segment_selections_product_level( $product_segmenting_table );
			$order_level_columns       = $this->get_segment_selections_order_level( $unique_orders_table );
			$segmenting_selections     = array(
				'product_level' => $this->prepare_selections( $product_level_columns ),
				'order_level'   => $this->prepare_selections( $order_level_columns ),
			);
			$this->report_columns      = array_merge( $product_level_columns, $order_level_columns );
			$segmenting_from          .= "INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)";
			$segmenting_groupby        = $product_segmenting_table . '.product_id';
			$segmenting_dimension_name = 'product_id';

			$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
		} elseif ( 'variation' === $this->query_args['segmentby'] ) {
			if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
				throw new ParameterException( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'woocommerce' ) );
			}

			$product_level_columns     = $this->get_segment_selections_product_level( $product_segmenting_table );
			$order_level_columns       = $this->get_segment_selections_order_level( $unique_orders_table );
			$segmenting_selections     = array(
				'product_level' => $this->prepare_selections( $product_level_columns ),
				'order_level'   => $this->prepare_selections( $order_level_columns ),
			);
			$this->report_columns      = array_merge( $product_level_columns, $order_level_columns );
			$segmenting_from          .= "INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)";
			$segmenting_where          = "AND $product_segmenting_table.product_id = {$this->query_args['product_includes'][0]}";
			$segmenting_groupby        = $product_segmenting_table . '.variation_id';
			$segmenting_dimension_name = 'variation_id';

			$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
		} elseif ( 'category' === $this->query_args['segmentby'] ) {
			$product_level_columns     = $this->get_segment_selections_product_level( $product_segmenting_table );
			$order_level_columns       = $this->get_segment_selections_order_level( $unique_orders_table );
			$segmenting_selections     = array(
				'product_level' => $this->prepare_selections( $product_level_columns ),
				'order_level'   => $this->prepare_selections( $order_level_columns ),
			);
			$this->report_columns      = array_merge( $product_level_columns, $order_level_columns );
			$segmenting_from          .= "
			INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)
			LEFT JOIN {$wpdb->term_relationships} ON {$product_segmenting_table}.product_id = {$wpdb->term_relationships}.object_id
			JOIN {$wpdb->term_taxonomy} ON {$wpdb->term_taxonomy}.term_taxonomy_id = {$wpdb->term_relationships}.term_taxonomy_id
			LEFT JOIN {$wpdb->wc_category_lookup} ON {$wpdb->term_taxonomy}.term_id = {$wpdb->wc_category_lookup}.category_id
			";
			$segmenting_where          = " AND {$wpdb->wc_category_lookup}.category_tree_id IS NOT NULL";
			$segmenting_groupby        = "{$wpdb->wc_category_lookup}.category_tree_id";
			$segmenting_dimension_name = 'category_id';

			$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
		} elseif ( 'coupon' === $this->query_args['segmentby'] ) {
			// As there can be 2 or more coupons applied per one order, coupon amount needs to be split.
			$coupon_override       = array(
				'coupons' => 'SUM(coupon_lookup.discount_amount) AS coupons',
			);
			$coupon_level_columns  = $this->segment_selections_orders( $table_name, $coupon_override );
			$segmenting_selections = $this->prepare_selections( $coupon_level_columns );
			$this->report_columns  = $coupon_level_columns;
			$segmenting_from      .= "
			INNER JOIN {$wpdb->prefix}wc_order_coupon_lookup AS coupon_lookup ON ($table_name.order_id = coupon_lookup.order_id)
            ";
			$segmenting_groupby    = 'coupon_lookup.coupon_id';

			$segments = $this->get_order_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
		} elseif ( 'customer_type' === $this->query_args['segmentby'] ) {
			$customer_level_columns = $this->segment_selections_orders( $table_name );
			$segmenting_selections  = $this->prepare_selections( $customer_level_columns );
			$this->report_columns   = $customer_level_columns;
			$segmenting_groupby     = "$table_name.returning_customer";

			$segments = $this->get_order_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
		}

		return $segments;
	}
}
Admin/API/Reports/Orders/Stats/Controller.php000064400000050522152333011040015027 0ustar00<?php
/**
 * REST API Reports orders stats controller
 *
 * Handles requests to the /reports/orders/stats endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Orders\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;

/**
 * REST API Reports orders stats controller class.
 *
 * @internal
 * @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
 */
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/orders/stats';

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args                        = array();
		$args['before']              = $request['before'];
		$args['after']               = $request['after'];
		$args['interval']            = $request['interval'];
		$args['page']                = $request['page'];
		$args['per_page']            = $request['per_page'];
		$args['orderby']             = $request['orderby'];
		$args['order']               = $request['order'];
		$args['fields']              = $request['fields'];
		$args['match']               = $request['match'];
		$args['status_is']           = (array) $request['status_is'];
		$args['status_is_not']       = (array) $request['status_is_not'];
		$args['product_includes']    = (array) $request['product_includes'];
		$args['product_excludes']    = (array) $request['product_excludes'];
		$args['variation_includes']  = (array) $request['variation_includes'];
		$args['variation_excludes']  = (array) $request['variation_excludes'];
		$args['coupon_includes']     = (array) $request['coupon_includes'];
		$args['coupon_excludes']     = (array) $request['coupon_excludes'];
		$args['tax_rate_includes']   = (array) $request['tax_rate_includes'];
		$args['tax_rate_excludes']   = (array) $request['tax_rate_excludes'];
		$args['customer_type']       = $request['customer_type'];
		$args['refunds']             = $request['refunds'];
		$args['attribute_is']        = (array) $request['attribute_is'];
		$args['attribute_is_not']    = (array) $request['attribute_is_not'];
		$args['category_includes']   = (array) $request['categories'];
		$args['segmentby']           = $request['segmentby'];
		$args['force_cache_refresh'] = $request['force_cache_refresh'];

		// For backwards compatibility, `customer` is aliased to `customer_type`.
		if ( empty( $request['customer_type'] ) && ! empty( $request['customer'] ) ) {
			$args['customer_type'] = $request['customer'];
		}

		return $args;
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$query_args   = $this->prepare_reports_query( $request );
		$orders_query = new Query( $query_args );
		try {
			$report_data = $orders_query->get_data();
		} catch ( ParameterException $e ) {
			return new \WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}

		$out_data = array(
			'totals'    => get_object_vars( $report_data->totals ),
			'intervals' => array(),
		);

		foreach ( $report_data->intervals as $interval_data ) {
			$item                    = $this->prepare_item_for_response( $interval_data, $request );
			$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $out_data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		$page      = $report_data->page_no;
		$max_pages = $report_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param Array           $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_orders_stats', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$data_values = array(
			'net_revenue'         => array(
				'description' => __( 'Net sales.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'format'      => 'currency',
			),
			'orders_count'        => array(
				'title'       => __( 'Orders', 'woocommerce' ),
				'description' => __( 'Number of orders', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
			),
			'avg_order_value'     => array(
				'description' => __( 'Average order value.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
				'format'      => 'currency',
			),
			'avg_items_per_order' => array(
				'description' => __( 'Average items per order', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'num_items_sold'      => array(
				'description' => __( 'Number of items sold', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'coupons'             => array(
				'description' => __( 'Amount discounted by coupons.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'coupons_count'       => array(
				'description' => __( 'Unique coupons count.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'total_customers'     => array(
				'description' => __( 'Total distinct customers.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'products'            => array(
				'description' => __( 'Number of distinct products sold.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
		);

		$segments = array(
			'segments' => array(
				'description' => __( 'Reports data grouped by segment condition.', 'woocommerce' ),
				'type'        => 'array',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'items'       => array(
					'type'       => 'object',
					'properties' => array(
						'segment_id' => array(
							'description' => __( 'Segment identificator.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'subtotals'  => array(
							'description' => __( 'Interval subtotals.', 'woocommerce' ),
							'type'        => 'object',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
							'properties'  => $data_values,
						),
					),
				),
			),
		);

		$totals = array_merge( $data_values, $segments );

		// Products is not shown in intervals.
		unset( $data_values['products'] );

		$intervals = array_merge( $data_values, $segments );

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_orders_stats',
			'type'       => 'object',
			'properties' => array(
				'totals'    => array(
					'description' => __( 'Totals data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'properties'  => $totals,
				),
				'intervals' => array(
					'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'interval'       => array(
								'description' => __( 'Type of interval.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'enum'        => array( 'day', 'week', 'month', 'year' ),
							),
							'date_start'     => array(
								'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_start_gmt' => array(
								'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end'       => array(
								'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end_gmt'   => array(
								'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'subtotals'      => array(
								'description' => __( 'Interval subtotals.', 'woocommerce' ),
								'type'        => 'object',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'properties'  => $intervals,
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                     = array();
		$params['context']          = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']             = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']         = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']            = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']           = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']            = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']          = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'date',
				'net_revenue',
				'orders_count',
				'avg_order_value',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['interval']         = array(
			'description'       => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'week',
			'enum'              => array(
				'hour',
				'day',
				'week',
				'month',
				'quarter',
				'year',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['match']            = array(
			'description'       => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'all',
			'enum'              => array(
				'all',
				'any',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['status_is']        = array(
			'description'       => __( 'Limit result set to items that have the specified order status.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'default'           => null,
			'items'             => array(
				'enum' => self::get_order_statuses(),
				'type' => 'string',
			),
		);
		$params['status_is_not']    = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified order status.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'enum' => self::get_order_statuses(),
				'type' => 'string',
			),
		);
		$params['product_includes'] = array(
			'description'       => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',

		);
		$params['product_excludes']   = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['variation_includes'] = array(
			'description'       => __( 'Limit result set to items that have the specified variation(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['variation_excludes'] = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified variation(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['coupon_includes']    = array(
			'description'       => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['coupon_excludes']    = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['tax_rate_includes']  = array(
			'description'       => __( 'Limit result set to items that have the specified tax rate(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['tax_rate_excludes']  = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified tax rate(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['customer']           = array(
			'description'       => __( 'Alias for customer_type (deprecated).', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array(
				'new',
				'returning',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['customer_type']      = array(
			'description'       => __( 'Limit result set to orders that have the specified customer_type', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array(
				'new',
				'returning',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['refunds']            = array(
			'description'       => __( 'Limit result set to specific types of refunds.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => '',
			'enum'              => array(
				'',
				'all',
				'partial',
				'full',
				'none',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['attribute_is']       = array(
			'description'       => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'array',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['attribute_is_not']   = array(
			'description'       => __( 'Limit result set to orders that don\'t include products with the specified attributes.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'array',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['segmentby']          = array(
			'description'       => __( 'Segment the response by additional constraint.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array(
				'product',
				'category',
				'variation',
				'coupon',
				'customer_type', // new vs returning.
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['fields']             = array(
			'description'       => __( 'Limit stats fields to the specified items.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
Admin/API/Reports/Orders/Controller.php000064400000051016152333011040013730 0ustar00<?php
/**
 * REST API Reports orders controller
 *
 * Handles requests to the /reports/orders endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Orders;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;

/**
 * REST API Reports orders controller class.
 *
 * @internal
 * @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
 */
class Controller extends ReportsController implements ExportableInterface {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/orders';

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args                        = array();
		$args['before']              = $request['before'];
		$args['after']               = $request['after'];
		$args['page']                = $request['page'];
		$args['per_page']            = $request['per_page'];
		$args['orderby']             = $request['orderby'];
		$args['order']               = $request['order'];
		$args['product_includes']    = (array) $request['product_includes'];
		$args['product_excludes']    = (array) $request['product_excludes'];
		$args['variation_includes']  = (array) $request['variation_includes'];
		$args['variation_excludes']  = (array) $request['variation_excludes'];
		$args['coupon_includes']     = (array) $request['coupon_includes'];
		$args['coupon_excludes']     = (array) $request['coupon_excludes'];
		$args['tax_rate_includes']   = (array) $request['tax_rate_includes'];
		$args['tax_rate_excludes']   = (array) $request['tax_rate_excludes'];
		$args['status_is']           = (array) $request['status_is'];
		$args['status_is_not']       = (array) $request['status_is_not'];
		$args['customer_type']       = $request['customer_type'];
		$args['extended_info']       = $request['extended_info'];
		$args['refunds']             = $request['refunds'];
		$args['match']               = $request['match'];
		$args['order_includes']      = $request['order_includes'];
		$args['order_excludes']      = $request['order_excludes'];
		$args['attribute_is']        = (array) $request['attribute_is'];
		$args['attribute_is_not']    = (array) $request['attribute_is_not'];
		$args['force_cache_refresh'] = $request['force_cache_refresh'];

		return $args;
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$query_args   = $this->prepare_reports_query( $request );
		$orders_query = new Query( $query_args );
		$report_data  = $orders_query->get_data();

		$data = array();

		foreach ( $report_data->data as $orders_data ) {
			$orders_data['order_number']    = $this->get_order_number( $orders_data['order_id'] );
			$orders_data['total_formatted'] = $this->get_total_formatted( $orders_data['order_id'] );
			$item                           = $this->prepare_item_for_response( $orders_data, $request );
			$data[]                         = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		$page      = $report_data->page_no;
		$max_pages = $report_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param stdClass        $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $report ) );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_orders', $response, $report, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WC_Reports_Query $object Object data.
	 * @return array
	 */
	protected function prepare_links( $object ) {
		$links = array(
			'order' => array(
				'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object['order_id'] ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_orders',
			'type'       => 'object',
			'properties' => array(
				'order_id'         => array(
					'description' => __( 'Order ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'order_number'     => array(
					'description' => __( 'Order Number.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'     => array(
					'description' => __( "Date the order was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt' => array(
					'description' => __( 'Date the order was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'status'           => array(
					'description' => __( 'Order status.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'customer_id'      => array(
					'description' => __( 'Customer ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'num_items_sold'   => array(
					'description' => __( 'Number of items sold.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'net_total'        => array(
					'description' => __( 'Net total revenue.', 'woocommerce' ),
					'type'        => 'float',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total_formatted'  => array(
					'description' => __( 'Net total revenue (formatted).', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'customer_type'    => array(
					'description' => __( 'Returning or new customer.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'extended_info'    => array(
					'products' => array(
						'type'        => 'array',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'List of order product IDs, names, quantities.', 'woocommerce' ),
					),
					'coupons'  => array(
						'type'        => 'array',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'List of order coupons.', 'woocommerce' ),
					),
					'customer' => array(
						'type'        => 'object',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Order customer information.', 'woocommerce' ),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                       = array();
		$params['context']            = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']               = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']           = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 0,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']              = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']             = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']              = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']            = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'date',
				'num_items_sold',
				'net_total',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['product_includes']   = array(
			'description'       => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['product_excludes']   = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['variation_includes'] = array(
			'description'       => __( 'Limit result set to items that have the specified variation(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['variation_excludes'] = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified variation(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['coupon_includes']    = array(
			'description'       => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['coupon_excludes']    = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['tax_rate_includes']  = array(
			'description'       => __( 'Limit result set to items that have the specified tax rate(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['tax_rate_excludes']  = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified tax rate(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['status_is']          = array(
			'description'       => __( 'Limit result set to items that have the specified order status.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'enum' => self::get_order_statuses(),
				'type' => 'string',
			),
		);
		$params['status_is_not']      = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified order status.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'enum' => self::get_order_statuses(),
				'type' => 'string',
			),
		);
		$params['customer_type']      = array(
			'description'       => __( 'Limit result set to returning or new customers.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => '',
			'enum'              => array(
				'',
				'returning',
				'new',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['refunds']            = array(
			'description'       => __( 'Limit result set to specific types of refunds.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => '',
			'enum'              => array(
				'',
				'all',
				'partial',
				'full',
				'none',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['extended_info']      = array(
			'description'       => __( 'Add additional piece of info about each coupon to the report.', 'woocommerce' ),
			'type'              => 'boolean',
			'default'           => false,
			'sanitize_callback' => 'wc_string_to_bool',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order_includes']     = array(
			'description'       => __( 'Limit result set to items that have the specified order ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['order_excludes']     = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified order ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['attribute_is']       = array(
			'description'       => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'array',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['attribute_is_not']   = array(
			'description'       => __( 'Limit result set to orders that don\'t include products with the specified attributes.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'array',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get customer name column export value.
	 *
	 * @param array $customer Customer from report row.
	 * @return string
	 */
	protected function get_customer_name( $customer ) {
		return $customer['first_name'] . ' ' . $customer['last_name'];
	}

	/**
	 * Get products column export value.
	 *
	 * @param array $products Products from report row.
	 * @return string
	 */
	protected function get_products( $products ) {
		$products_list = array();

		foreach ( $products as $product ) {
			$products_list[] = sprintf(
				/* translators: 1: numeric product quantity, 2: name of product */
				__( '%1$s× %2$s', 'woocommerce' ),
				$product['quantity'],
				$product['name']
			);
		}

		return implode( ', ', $products_list );
	}

	/**
	 * Get coupons column export value.
	 *
	 * @param array $coupons Coupons from report row.
	 * @return string
	 */
	protected function get_coupons( $coupons ) {
		return implode( ', ', wp_list_pluck( $coupons, 'code' ) );
	}

	/**
	 * Get the column names for export.
	 *
	 * @return array Key value pair of Column ID => Label.
	 */
	public function get_export_columns() {
		$export_columns = array(
			'date_created'    => __( 'Date', 'woocommerce' ),
			'order_number'    => __( 'Order #', 'woocommerce' ),
			'total_formatted' => __( 'N. Revenue (formatted)', 'woocommerce' ),
			'status'          => __( 'Status', 'woocommerce' ),
			'customer_name'   => __( 'Customer', 'woocommerce' ),
			'customer_type'   => __( 'Customer type', 'woocommerce' ),
			'products'        => __( 'Product(s)', 'woocommerce' ),
			'num_items_sold'  => __( 'Items sold', 'woocommerce' ),
			'coupons'         => __( 'Coupon(s)', 'woocommerce' ),
			'net_total'       => __( 'N. Revenue', 'woocommerce' ),
		);

		/**
		 * Filter to add or remove column names from the orders report for
		 * export.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_report_orders_export_columns',
			$export_columns
		);
	}

	/**
	 * Get the column values for export.
	 *
	 * @param array $item Single report item/row.
	 * @return array Key value pair of Column ID => Row Value.
	 */
	public function prepare_item_for_export( $item ) {
		$export_item = array(
			'date_created'    => $item['date_created'],
			'order_number'    => $item['order_number'],
			'total_formatted' => $item['total_formatted'],
			'status'          => $item['status'],
			'customer_name'   => isset( $item['extended_info']['customer'] ) ? $this->get_customer_name( $item['extended_info']['customer'] ) : null,
			'customer_type'   => $item['customer_type'],
			'products'        => isset( $item['extended_info']['products'] ) ? $this->get_products( $item['extended_info']['products'] ) : null,
			'num_items_sold'  => $item['num_items_sold'],
			'coupons'         => isset( $item['extended_info']['coupons'] ) ? $this->get_coupons( $item['extended_info']['coupons'] ) : null,
			'net_total'       => $item['net_total'],
		);

		/**
		 * Filter to prepare extra columns in the export item for the orders
		 * report.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_report_orders_prepare_export_item',
			$export_item,
			$item
		);
	}
}
Admin/API/Reports/Orders/DataStore.php000064400000045052152333011040013476 0ustar00<?php
/**
 * API\Reports\Orders\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Orders;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;


/**
 * API\Reports\Orders\DataStore.
 */
class DataStore extends ReportsDataStore implements DataStoreInterface {

	/**
	 * Table used to get the data.
	 *
	 * @var string
	 */
	protected static $table_name = 'wc_order_stats';

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'orders';

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'order_id'         => 'intval',
		'parent_id'        => 'intval',
		'date_created'     => 'strval',
		'date_created_gmt' => 'strval',
		'status'           => 'strval',
		'customer_id'      => 'intval',
		'net_total'        => 'floatval',
		'total_sales'      => 'floatval',
		'num_items_sold'   => 'intval',
		'customer_type'    => 'strval',
	);

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'orders';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$table_name = self::get_db_table_name();
		// Avoid ambigious columns in SQL query.
		$this->report_columns = array(
			'order_id'         => "DISTINCT {$table_name}.order_id",
			'parent_id'        => "{$table_name}.parent_id",
			'date_created'     => "{$table_name}.date_created",
			'date_created_gmt' => "{$table_name}.date_created_gmt",
			'status'           => "REPLACE({$table_name}.status, 'wc-', '') as status",
			'customer_id'      => "{$table_name}.customer_id",
			'net_total'        => "{$table_name}.net_total",
			'total_sales'      => "{$table_name}.total_sales",
			'num_items_sold'   => "{$table_name}.num_items_sold",
			'customer_type'    => "(CASE WHEN {$table_name}.returning_customer = 0 THEN 'new' ELSE 'returning' END) as customer_type",
		);
	}

	/**
	 * Updates the database query with parameters used for orders report: coupons and products filters.
	 *
	 * @param array $query_args Query arguments supplied by the user.
	 */
	protected function add_sql_query_params( $query_args ) {
		global $wpdb;
		$order_stats_lookup_table   = self::get_db_table_name();
		$order_coupon_lookup_table  = $wpdb->prefix . 'wc_order_coupon_lookup';
		$order_product_lookup_table = $wpdb->prefix . 'wc_order_product_lookup';
		$order_tax_lookup_table     = $wpdb->prefix . 'wc_order_tax_lookup';
		$operator                   = $this->get_match_operator( $query_args );
		$where_subquery             = array();
		$have_joined_products_table = false;

		$this->add_time_period_sql_params( $query_args, $order_stats_lookup_table );
		$this->get_limit_sql_params( $query_args );
		$this->add_order_by_sql_params( $query_args );

		$status_subquery = $this->get_status_subquery( $query_args );
		if ( $status_subquery ) {
			if ( empty( $query_args['status_is'] ) && empty( $query_args['status_is_not'] ) ) {
				$this->subquery->add_sql_clause( 'where', "AND {$status_subquery}" );
			} else {
				$where_subquery[] = $status_subquery;
			}
		}

		$included_orders = $this->get_included_orders( $query_args );
		if ( $included_orders ) {
			$where_subquery[] = "{$order_stats_lookup_table}.order_id IN ({$included_orders})";
		}

		$excluded_orders = $this->get_excluded_orders( $query_args );
		if ( $excluded_orders ) {
			$where_subquery[] = "{$order_stats_lookup_table}.order_id NOT IN ({$excluded_orders})";
		}

		if ( $query_args['customer_type'] ) {
			$returning_customer = 'returning' === $query_args['customer_type'] ? 1 : 0;
			$where_subquery[]   = "{$order_stats_lookup_table}.returning_customer = ${returning_customer}";
		}

		$refund_subquery = $this->get_refund_subquery( $query_args );
		$this->subquery->add_sql_clause( 'from', $refund_subquery['from_clause'] );
		if ( $refund_subquery['where_clause'] ) {
			$where_subquery[] = $refund_subquery['where_clause'];
		}

		$included_coupons = $this->get_included_coupons( $query_args );
		$excluded_coupons = $this->get_excluded_coupons( $query_args );
		if ( $included_coupons || $excluded_coupons ) {
			$this->subquery->add_sql_clause( 'join', "LEFT JOIN {$order_coupon_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_coupon_lookup_table}.order_id" );
		}
		if ( $included_coupons ) {
			$where_subquery[] = "{$order_coupon_lookup_table}.coupon_id IN ({$included_coupons})";
		}
		if ( $excluded_coupons ) {
			$where_subquery[] = "({$order_coupon_lookup_table}.coupon_id IS NULL OR {$order_coupon_lookup_table}.coupon_id NOT IN ({$excluded_coupons}))";
		}

		$included_products = $this->get_included_products( $query_args );
		$excluded_products = $this->get_excluded_products( $query_args );
		if ( $included_products || $excluded_products ) {
			$this->subquery->add_sql_clause( 'join', "LEFT JOIN {$order_product_lookup_table} product_lookup" );
			$this->subquery->add_sql_clause( 'join', "ON {$order_stats_lookup_table}.order_id = product_lookup.order_id" );
		}
		if ( $included_products ) {
			$this->subquery->add_sql_clause( 'join', "AND product_lookup.product_id IN ({$included_products})" );
			$where_subquery[] = 'product_lookup.order_id IS NOT NULL';
		}
		if ( $excluded_products ) {
			$this->subquery->add_sql_clause( 'join', "AND product_lookup.product_id IN ({$excluded_products})" );
			$where_subquery[] = 'product_lookup.order_id IS NULL';
		}

		$included_variations = $this->get_included_variations( $query_args );
		$excluded_variations = $this->get_excluded_variations( $query_args );
		if ( $included_variations || $excluded_variations ) {
			$this->subquery->add_sql_clause( 'join', "LEFT JOIN {$order_product_lookup_table} variation_lookup" );
			$this->subquery->add_sql_clause( 'join', "ON {$order_stats_lookup_table}.order_id = variation_lookup.order_id" );
		}
		if ( $included_variations ) {
			$this->subquery->add_sql_clause( 'join', "AND variation_lookup.variation_id IN ({$included_variations})" );
			$where_subquery[] = 'variation_lookup.order_id IS NOT NULL';
		}
		if ( $excluded_variations ) {
			$this->subquery->add_sql_clause( 'join', "AND variation_lookup.variation_id IN ({$excluded_variations})" );
			$where_subquery[] = 'variation_lookup.order_id IS NULL';
		}

		$included_tax_rates = ! empty( $query_args['tax_rate_includes'] ) ? implode( ',', array_map( 'esc_sql', $query_args['tax_rate_includes'] ) ) : false;
		$excluded_tax_rates = ! empty( $query_args['tax_rate_excludes'] ) ? implode( ',', array_map( 'esc_sql', $query_args['tax_rate_excludes'] ) ) : false;
		if ( $included_tax_rates || $excluded_tax_rates ) {
			$this->subquery->add_sql_clause( 'join', "LEFT JOIN {$order_tax_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_tax_lookup_table}.order_id" );
		}
		if ( $included_tax_rates ) {
			$where_subquery[] = "{$order_tax_lookup_table}.tax_rate_id IN ({$included_tax_rates})";
		}
		if ( $excluded_tax_rates ) {
			$where_subquery[] = "{$order_tax_lookup_table}.tax_rate_id NOT IN ({$excluded_tax_rates}) OR {$order_tax_lookup_table}.tax_rate_id IS NULL";
		}

		$attribute_subqueries = $this->get_attribute_subqueries( $query_args, $order_stats_lookup_table );
		if ( $attribute_subqueries['join'] && $attribute_subqueries['where'] ) {
			$this->subquery->add_sql_clause( 'join', "JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id" );

			// Add JOINs for matching attributes.
			foreach ( $attribute_subqueries['join'] as $attribute_join ) {
				$this->subquery->add_sql_clause( 'join', $attribute_join );
			}
			// Add WHEREs for matching attributes.
			$where_subquery = array_merge( $where_subquery, $attribute_subqueries['where'] );
		}

		if ( 0 < count( $where_subquery ) ) {
			$this->subquery->add_sql_clause( 'where', 'AND (' . implode( " {$operator} ", $where_subquery ) . ')' );
		}
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page'          => get_option( 'posts_per_page' ),
			'page'              => 1,
			'order'             => 'DESC',
			'orderby'           => 'date_created',
			'before'            => TimeInterval::default_before(),
			'after'             => TimeInterval::default_after(),
			'fields'            => '*',
			'product_includes'  => array(),
			'product_excludes'  => array(),
			'coupon_includes'   => array(),
			'coupon_excludes'   => array(),
			'tax_rate_includes' => array(),
			'tax_rate_excludes' => array(),
			'customer_type'     => null,
			'status_is'         => array(),
			'extended_info'     => false,
			'refunds'           => null,
			'order_includes'    => array(),
			'order_excludes'    => array(),
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$data = (object) array(
				'data'    => array(),
				'total'   => 0,
				'pages'   => 0,
				'page_no' => 0,
			);

			$selections = $this->selected_columns( $query_args );
			$params     = $this->get_limit_params( $query_args );
			$this->add_sql_query_params( $query_args );
			/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
			$db_records_count = (int) $wpdb->get_var(
				"SELECT COUNT(*) FROM (
					{$this->subquery->get_query_statement()}
				) AS tt"
			);
			/* phpcs:enable */

			if ( 0 === $params['per_page'] ) {
				$total_pages = 0;
			} else {
				$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
			}
			if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
				$data = (object) array(
					'data'    => array(),
					'total'   => $db_records_count,
					'pages'   => 0,
					'page_no' => 0,
				);
				return $data;
			}

			$this->subquery->clear_sql_clause( 'select' );
			$this->subquery->add_sql_clause( 'select', $selections );
			$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
			$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
			/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
			$orders_data = $wpdb->get_results(
				$this->subquery->get_query_statement(),
				ARRAY_A
			);
			/* phpcs:enable */

			if ( null === $orders_data ) {
				return $data;
			}

			if ( $query_args['extended_info'] ) {
				$this->include_extended_info( $orders_data, $query_args );
			}

			$orders_data = array_map( array( $this, 'cast_numbers' ), $orders_data );
			$data        = (object) array(
				'data'    => $orders_data,
				'total'   => $db_records_count,
				'pages'   => $total_pages,
				'page_no' => (int) $query_args['page'],
			);

			$this->set_cached_data( $cache_key, $data );
		}
		return $data;
	}

	/**
	 * Normalizes order_by clause to match to SQL query.
	 *
	 * @param string $order_by Order by option requeste by user.
	 * @return string
	 */
	protected function normalize_order_by( $order_by ) {
		if ( 'date' === $order_by ) {
			return 'date_created';
		}

		return $order_by;
	}

	/**
	 * Enriches the order data.
	 *
	 * @param array $orders_data Orders data.
	 * @param array $query_args  Query parameters.
	 */
	protected function include_extended_info( &$orders_data, $query_args ) {
		$mapped_orders    = $this->map_array_by_key( $orders_data, 'order_id' );
		$related_orders   = $this->get_orders_with_parent_id( $mapped_orders );
		$order_ids        = array_merge( array_keys( $mapped_orders ), array_keys( $related_orders ) );
		$products         = $this->get_products_by_order_ids( $order_ids );
		$coupons          = $this->get_coupons_by_order_ids( array_keys( $mapped_orders ) );
		$customers        = $this->get_customers_by_orders( $orders_data );
		$mapped_customers = $this->map_array_by_key( $customers, 'customer_id' );

		$mapped_data = array();
		foreach ( $products as $product ) {
			if ( ! isset( $mapped_data[ $product['order_id'] ] ) ) {
				$mapped_data[ $product['order_id'] ]['products'] = array();
			}

			$is_variation = '0' !== $product['variation_id'];
			$product_data = array(
				'id'       => $is_variation ? $product['variation_id'] : $product['product_id'],
				'name'     => $product['product_name'],
				'quantity' => $product['product_quantity'],
			);

			if ( $is_variation ) {
				$variation = wc_get_product( $product_data['id'] );
				/**
				 * Used to determine the separator for products and their variations titles.
				 *
				 * @since 4.0.0
				 */
				$separator = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $variation );

				if ( false === strpos( $product_data['name'], $separator ) ) {
					$attributes            = wc_get_formatted_variation( $variation, true, false );
					$product_data['name'] .= $separator . $attributes;
				}
			}

			$mapped_data[ $product['order_id'] ]['products'][] = $product_data;

			// If this product's order has another related order, it will be added to our mapped_data.
			if ( isset( $related_orders [ $product['order_id'] ] ) ) {
				$mapped_data[ $related_orders[ $product['order_id'] ]['order_id'] ] ['products'] [] = $product_data;
			}
		}

		foreach ( $coupons as $coupon ) {
			if ( ! isset( $mapped_data[ $coupon['order_id'] ] ) ) {
				$mapped_data[ $product['order_id'] ]['coupons'] = array();
			}

			$mapped_data[ $coupon['order_id'] ]['coupons'][] = array(
				'id'   => $coupon['coupon_id'],
				'code' => wc_format_coupon_code( $coupon['coupon_code'] ),
			);
		}

		foreach ( $orders_data as $key => $order_data ) {
			$defaults                             = array(
				'products' => array(),
				'coupons'  => array(),
				'customer' => array(),
			);
			$orders_data[ $key ]['extended_info'] = isset( $mapped_data[ $order_data['order_id'] ] ) ? array_merge( $defaults, $mapped_data[ $order_data['order_id'] ] ) : $defaults;
			if ( $order_data['customer_id'] && isset( $mapped_customers[ $order_data['customer_id'] ] ) ) {
				$orders_data[ $key ]['extended_info']['customer'] = $mapped_customers[ $order_data['customer_id'] ];
			}
		}
	}

	/**
	 * Returns oreders that have a parent id
	 *
	 * @param array $orders Orders array.
	 * @return array
	 */
	protected function get_orders_with_parent_id( $orders ) {
		$related_orders = array();
		foreach ( $orders as $order ) {
			if ( '0' !== $order['parent_id'] ) {
				$related_orders[ $order['parent_id'] ] = $order;
			}
		}
		return $related_orders;
	}

	/**
	 * Returns the same array index by a given key
	 *
	 * @param array  $array Array to be looped over.
	 * @param string $key Key of values used for new array.
	 * @return array
	 */
	protected function map_array_by_key( $array, $key ) {
		$mapped = array();
		foreach ( $array as $item ) {
			$mapped[ $item[ $key ] ] = $item;
		}
		return $mapped;
	}

	/**
	 * Get product IDs, names, and quantity from order IDs.
	 *
	 * @param array $order_ids Array of order IDs.
	 * @return array
	 */
	protected function get_products_by_order_ids( $order_ids ) {
		global $wpdb;
		$order_product_lookup_table = $wpdb->prefix . 'wc_order_product_lookup';
		$included_order_ids         = implode( ',', $order_ids );

		/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
		$products = $wpdb->get_results(
			"SELECT
				order_id,
				product_id,
				variation_id,
				post_title as product_name,
				product_qty as product_quantity
			FROM {$wpdb->posts}
			JOIN
				{$order_product_lookup_table}
				ON {$wpdb->posts}.ID = (
					CASE WHEN variation_id > 0
						THEN variation_id
						ELSE product_id
					END
				)
			WHERE
				order_id IN ({$included_order_ids})
			",
			ARRAY_A
		);
		/* phpcs:enable */

		return $products;
	}

	/**
	 * Get customer data from Order data.
	 *
	 * @param array $orders Array of orders data.
	 * @return array
	 */
	protected function get_customers_by_orders( $orders ) {
		global $wpdb;

		$customer_lookup_table = $wpdb->prefix . 'wc_customer_lookup';
		$customer_ids          = array();

		foreach ( $orders as $order ) {
			if ( $order['customer_id'] ) {
				$customer_ids[] = intval( $order['customer_id'] );
			}
		}

		if ( empty( $customer_ids ) ) {
			return array();
		}

		/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
		$customer_ids = implode( ',', $customer_ids );
		$customers    = $wpdb->get_results(
			"SELECT * FROM {$customer_lookup_table} WHERE customer_id IN ({$customer_ids})",
			ARRAY_A
		);
		/* phpcs:enable */

		return $customers;
	}

	/**
	 * Get coupon information from order IDs.
	 *
	 * @param array $order_ids Array of order IDs.
	 * @return array
	 */
	protected function get_coupons_by_order_ids( $order_ids ) {
		global $wpdb;
		$order_coupon_lookup_table = $wpdb->prefix . 'wc_order_coupon_lookup';
		$included_order_ids        = implode( ',', $order_ids );

		/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
		$coupons = $wpdb->get_results(
			"SELECT order_id, coupon_id, post_title as coupon_code
				FROM {$wpdb->posts}
				JOIN {$order_coupon_lookup_table} ON {$order_coupon_lookup_table}.coupon_id = {$wpdb->posts}.ID
				WHERE
					order_id IN ({$included_order_ids})
				",
			ARRAY_A
		);
		/* phpcs:enable */

		return $coupons;
	}

	/**
	 * Get all statuses that have been synced.
	 *
	 * @return array Unique order statuses.
	 */
	public static function get_all_statuses() {
		global $wpdb;

		$cache_key = 'orders-all-statuses';
		$statuses  = Cache::get( $cache_key );

		if ( false === $statuses ) {
			/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
			$table_name = self::get_db_table_name();
			$statuses   = $wpdb->get_col(
				"SELECT DISTINCT status FROM {$table_name}"
			);
			/* phpcs:enable */

			Cache::set( $cache_key, $statuses );
		}

		return $statuses;
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		$this->subquery = new SqlQuery( $this->context . '_subquery' );
		$this->subquery->add_sql_clause( 'select', self::get_db_table_name() . '.order_id' );
		$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
	}
}
Admin/API/Reports/DataStoreInterface.php000064400000000621152333011040014052 0ustar00<?php
/**
 * Reports Data Store Interface
 */

namespace Automattic\WooCommerce\Admin\API\Reports;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WooCommerce Reports data store interface.
 *
 * @since 3.5.0
 */
interface DataStoreInterface {

	/**
	 * Get the data based on args.
	 *
	 * @param array $args Query parameters.
	 * @return stdClass|WP_Error
	 */
	public function get_data( $args );
}
Admin/API/Reports/ExportableTraits.php000064400000001160152333011040013636 0ustar00<?php
/**
 * REST API Reports exportable traits
 *
 * Collection of utility methods for exportable reports.
 */

namespace Automattic\WooCommerce\Admin\API\Reports;

defined( 'ABSPATH' ) || exit;

/**
 * ExportableTraits class.
 */
trait ExportableTraits {
	/**
	 * Format numbers for CSV using store precision setting.
	 *
	 * @param string|float $value Numeric value.
	 * @return string Formatted value.
	 */
	public static function csv_number_format( $value ) {
		$decimals = wc_get_price_decimals();
		// See: @woocommerce/currency: getCurrencyFormatDecimal().
		return number_format( $value, $decimals, '.', '' );
	}
}
Admin/API/Reports/Import/Controller.php000064400000021246152333011040013746 0ustar00<?php
/**
 * REST API Reports Import Controller
 *
 * Handles requests to /reports/import
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Import;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\ReportsSync;

/**
 * Reports Imports controller.
 *
 * @internal
 * @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
 */
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/import';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'import_items' ),
					'permission_callback' => array( $this, 'import_permissions_check' ),
					'args'                => $this->get_import_collection_params(),
				),
				'schema' => array( $this, 'get_import_public_schema' ),
			)
		);
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/cancel',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'cancel_import' ),
					'permission_callback' => array( $this, 'import_permissions_check' ),
				),
				'schema' => array( $this, 'get_import_public_schema' ),
			)
		);
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/delete',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'delete_imported_items' ),
					'permission_callback' => array( $this, 'import_permissions_check' ),
				),
				'schema' => array( $this, 'get_import_public_schema' ),
			)
		);
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/status',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_import_status' ),
					'permission_callback' => array( $this, 'import_permissions_check' ),
				),
				'schema' => array( $this, 'get_import_public_schema' ),
			)
		);
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/totals',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_import_totals' ),
					'permission_callback' => array( $this, 'import_permissions_check' ),
					'args'                => $this->get_import_collection_params(),
				),
				'schema' => array( $this, 'get_import_public_schema' ),
			)
		);
	}

	/**
	 * Makes sure the current user has access to WRITE the settings APIs.
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|bool
	 */
	public function import_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Import data based on user request params.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function import_items( $request ) {
		$query_args = $this->prepare_objects_query( $request );
		$import     = ReportsSync::regenerate_report_data( $query_args['days'], $query_args['skip_existing'] );

		if ( is_wp_error( $import ) ) {
			$result = array(
				'status'  => 'error',
				'message' => $import->get_error_message(),
			);
		} else {
			$result = array(
				'status'  => 'success',
				'message' => $import,
			);
		}

		$response = $this->prepare_item_for_response( $result, $request );
		$data     = $this->prepare_response_for_collection( $response );

		return rest_ensure_response( $data );
	}

	/**
	 * Prepare request object as query args.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args                  = array();
		$args['skip_existing'] = $request['skip_existing'];
		$args['days']          = $request['days'];

		return $args;
	}

	/**
	 * Prepare the data object for response.
	 *
	 * @param object          $item Data object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data     = $this->add_additional_fields_to_object( $item, $request );
		$data     = $this->filter_response_by_context( $data, 'view' );
		$response = rest_ensure_response( $data );

		/**
		 * Filter the list returned from the API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param array            $item     The original item.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_reports_import', $response, $item, $request );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_import_collection_params() {
		$params                  = array();
		$params['days']          = array(
			'description'       => __( 'Number of days to import.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 0,
		);
		$params['skip_existing'] = array(
			'description'       => __( 'Skip importing existing order data.', 'woocommerce' ),
			'type'              => 'boolean',
			'default'           => false,
			'sanitize_callback' => 'wc_string_to_bool',
			'validate_callback' => 'rest_validate_request_arg',
		);
		return $params;
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_import_public_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_import',
			'type'       => 'object',
			'properties' => array(
				'status'  => array(
					'description' => __( 'Regeneration status.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'message' => array(
					'description' => __( 'Regenerate data message.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Cancel all queued import actions.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function cancel_import( $request ) {
		ReportsSync::clear_queued_actions();

		$result = array(
			'status'  => 'success',
			'message' => __( 'All pending and in-progress import actions have been cancelled.', 'woocommerce' ),
		);

		$response = $this->prepare_item_for_response( $result, $request );
		$data     = $this->prepare_response_for_collection( $response );

		return rest_ensure_response( $data );
	}

	/**
	 * Delete all imported items.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function delete_imported_items( $request ) {
		$delete = ReportsSync::delete_report_data();

		if ( is_wp_error( $delete ) ) {
			$result = array(
				'status'  => 'error',
				'message' => $delete->get_error_message(),
			);
		} else {
			$result = array(
				'status'  => 'success',
				'message' => $delete,
			);
		}

		$response = $this->prepare_item_for_response( $result, $request );
		$data     = $this->prepare_response_for_collection( $response );

		return rest_ensure_response( $data );
	}

	/**
	 * Get the status of the current import.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_import_status( $request ) {
		$result   = ReportsSync::get_import_stats();
		$response = $this->prepare_item_for_response( $result, $request );
		$data     = $this->prepare_response_for_collection( $response );

		return rest_ensure_response( $data );
	}

	/**
	 * Get the total orders and customers based on user supplied params.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_import_totals( $request ) {
		$query_args = $this->prepare_objects_query( $request );
		$totals     = ReportsSync::get_import_totals( $query_args['days'], $query_args['skip_existing'] );

		$response = $this->prepare_item_for_response( $totals, $request );
		$data     = $this->prepare_response_for_collection( $response );

		return rest_ensure_response( $data );
	}
}
Admin/API/Reports/Stock/Stats/DataStore.php000064400000010567152333011040014424 0ustar00<?php
/**
 * API\Reports\Stock\Stats\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Stock\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;

/**
 * API\Reports\Stock\Stats\DataStore.
 */
class DataStore extends ReportsDataStore implements DataStoreInterface {

	/**
	 * Get stock counts for the whole store.
	 *
	 * @param array $query Not used for the stock stats data store, but needed for the interface.
	 * @return array Array of counts.
	 */
	public function get_data( $query ) {
		$report_data              = array();
		$cache_expire             = DAY_IN_SECONDS * 30;
		$low_stock_transient_name = 'wc_admin_stock_count_lowstock';
		$low_stock_count          = get_transient( $low_stock_transient_name );
		if ( false === $low_stock_count ) {
			$low_stock_count = $this->get_low_stock_count();
			set_transient( $low_stock_transient_name, $low_stock_count, $cache_expire );
		} else {
			$low_stock_count = intval( $low_stock_count );
		}
		$report_data['lowstock'] = $low_stock_count;

		$status_options = wc_get_product_stock_status_options();
		foreach ( $status_options as $status => $label ) {
			$transient_name = 'wc_admin_stock_count_' . $status;
			$count          = get_transient( $transient_name );
			if ( false === $count ) {
				$count = $this->get_count( $status );
				set_transient( $transient_name, $count, $cache_expire );
			} else {
				$count = intval( $count );
			}
			$report_data[ $status ] = $count;
		}

		$product_count_transient_name = 'wc_admin_product_count';
		$product_count                = get_transient( $product_count_transient_name );
		if ( false === $product_count ) {
			$product_count = $this->get_product_count();
			set_transient( $product_count_transient_name, $product_count, $cache_expire );
		} else {
			$product_count = intval( $product_count );
		}
		$report_data['products'] = $product_count;
		return $report_data;
	}

	/**
	 * Get low stock count (products with stock < low stock amount, but greater than no stock amount).
	 *
	 * @return int Low stock count.
	 */
	private function get_low_stock_count() {
		global $wpdb;

		$no_stock_amount  = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) );
		$low_stock_amount = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );

		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"
				SELECT count( DISTINCT posts.ID ) FROM {$wpdb->posts} posts
				LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id
				LEFT JOIN {$wpdb->postmeta} low_stock_amount_meta ON posts.ID = low_stock_amount_meta.post_id AND low_stock_amount_meta.meta_key = '_low_stock_amount'
				WHERE posts.post_type IN ( 'product', 'product_variation' )
				AND wc_product_meta_lookup.stock_quantity IS NOT NULL
				AND wc_product_meta_lookup.stock_status = 'instock'
				AND (
					(
						low_stock_amount_meta.meta_value > ''
						AND wc_product_meta_lookup.stock_quantity <= CAST(low_stock_amount_meta.meta_value AS SIGNED)
						AND wc_product_meta_lookup.stock_quantity > %d
					)
					OR (
						(
							low_stock_amount_meta.meta_value IS NULL OR low_stock_amount_meta.meta_value <= ''
						)
						AND wc_product_meta_lookup.stock_quantity <= %d
						AND wc_product_meta_lookup.stock_quantity > %d
					)
				)
				",
				$no_stock_amount,
				$low_stock_amount,
				$no_stock_amount
			)
		);
	}

	/**
	 * Get count for the passed in stock status.
	 *
	 * @param  string $status Status slug.
	 * @return int Count.
	 */
	private function get_count( $status ) {
		global $wpdb;

		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"
				SELECT count( DISTINCT posts.ID ) FROM {$wpdb->posts} posts
				LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id
				WHERE posts.post_type IN ( 'product', 'product_variation' )
				AND wc_product_meta_lookup.stock_status = %s
				",
				$status
			)
		);
	}

	/**
	 * Get product count for the store.
	 *
	 * @return int Product count.
	 */
	private function get_product_count() {
		$query_args              = array();
		$query_args['post_type'] = array( 'product', 'product_variation' );
		$query                   = new \WP_Query();
		$query->query( $query_args );
		return intval( $query->found_posts );
	}
}
Admin/API/Reports/Stock/Stats/Query.php000064400000001317152333011040013634 0ustar00<?php
/**
 * Class for stock stats report querying
 *
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\Query();
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Stock\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Stock\Stats\Query
 */
class Query extends ReportsQuery {

	/**
	 * Get product data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$data_store = \WC_Data_Store::load( 'report-stock-stats' );
		$results    = $data_store->get_data();
		return apply_filters( 'woocommerce_analytics_stock_stats_query', $results );
	}
}
Admin/API/Reports/Stock/Stats/Controller.php000064400000007111152333011040014650 0ustar00<?php
/**
 * REST API Reports stock stats controller
 *
 * Handles requests to the /reports/stock/stats endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Stock\Stats;

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports stock stats controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/stock/stats';

	/**
	 * Get Stock Status Totals.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$stock_query = new Query();
		$report_data = $stock_query->get_data();
		$out_data    = array(
			'totals' => $report_data,
		);
		return rest_ensure_response( $out_data );
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param  WC_Product      $report  Report data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WC_Product       $product   The original bject.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_stock_stats', $response, $product, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$totals = array(
			'products' => array(
				'description' => __( 'Number of products.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'lowstock' => array(
				'description' => __( 'Number of low stock products.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
		);

		$status_options = wc_get_product_stock_status_options();
		foreach ( $status_options as $status => $label ) {
			$totals[ $status ] = array(
				/* translators: Stock status. Example: "Number of low stock products */
				'description' => sprintf( __( 'Number of %s products.', 'woocommerce' ), $label ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			);
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_customers_stats',
			'type'       => 'object',
			'properties' => array(
				'totals' => array(
					'description' => __( 'Totals data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'properties'  => $totals,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params            = array();
		$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
		return $params;
	}
}
Admin/API/Reports/Stock/Controller.php000064400000044313152333011040013557 0ustar00<?php
/**
 * REST API Reports stock controller
 *
 * Handles requests to the /reports/stock endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Stock;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;

/**
 * REST API Reports stock controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/stock';

	/**
	 * Registered stock status options.
	 *
	 * @var array
	 */
	protected $status_options;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->status_options = wc_get_product_stock_status_options();
	}

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param  WP_REST_Request $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args                        = array();
		$args['offset']              = $request['offset'];
		$args['order']               = $request['order'];
		$args['orderby']             = $request['orderby'];
		$args['paged']               = $request['page'];
		$args['post__in']            = $request['include'];
		$args['post__not_in']        = $request['exclude'];
		$args['posts_per_page']      = $request['per_page'];
		$args['post_parent__in']     = $request['parent'];
		$args['post_parent__not_in'] = $request['parent_exclude'];

		if ( 'date' === $args['orderby'] ) {
			$args['orderby'] = 'date ID';
		} elseif ( 'include' === $args['orderby'] ) {
			$args['orderby'] = 'post__in';
		} elseif ( 'id' === $args['orderby'] ) {
			$args['orderby'] = 'ID'; // ID must be capitalized.
		}

		$args['post_type'] = array( 'product', 'product_variation' );

		if ( 'lowstock' === $request['type'] ) {
			$args['low_in_stock'] = true;
		} elseif ( in_array( $request['type'], array_keys( $this->status_options ), true ) ) {
			$args['stock_status'] = $request['type'];
		}

		$args['ignore_sticky_posts'] = true;

		return $args;
	}

	/**
	 * Query products.
	 *
	 * @param  array $query_args Query args.
	 * @return array
	 */
	protected function get_products( $query_args ) {
		$query  = new \WP_Query();
		$result = $query->query( $query_args );

		$total_posts = $query->found_posts;
		if ( $total_posts < 1 ) {
			// Out-of-bounds, run the query again without LIMIT for total count.
			unset( $query_args['paged'] );
			$count_query = new \WP_Query();
			$count_query->query( $query_args );
			$total_posts = $count_query->found_posts;
		}

		return array(
			'objects' => array_map( 'wc_get_product', $result ),
			'total'   => (int) $total_posts,
			'pages'   => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ),
		);
	}

	/**
	 * Get all reports.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		add_filter( 'posts_where', array( __CLASS__, 'add_wp_query_filter' ), 10, 2 );
		add_filter( 'posts_join', array( __CLASS__, 'add_wp_query_join' ), 10, 2 );
		add_filter( 'posts_groupby', array( __CLASS__, 'add_wp_query_group_by' ), 10, 2 );
		add_filter( 'posts_clauses', array( __CLASS__, 'add_wp_query_orderby' ), 10, 2 );
		$query_args    = $this->prepare_reports_query( $request );
		$query_results = $this->get_products( $query_args );
		remove_filter( 'posts_where', array( __CLASS__, 'add_wp_query_filter' ), 10 );
		remove_filter( 'posts_join', array( __CLASS__, 'add_wp_query_join' ), 10 );
		remove_filter( 'posts_groupby', array( __CLASS__, 'add_wp_query_group_by' ), 10 );
		remove_filter( 'posts_clauses', array( __CLASS__, 'add_wp_query_orderby' ), 10 );

		$objects = array();
		foreach ( $query_results['objects'] as $object ) {
			$data      = $this->prepare_item_for_response( $object, $request );
			$objects[] = $this->prepare_response_for_collection( $data );
		}

		$page      = (int) $query_args['paged'];
		$max_pages = $query_results['pages'];

		$response = rest_ensure_response( $objects );
		$response->header( 'X-WP-Total', $query_results['total'] );
		$response->header( 'X-WP-TotalPages', (int) $max_pages );

		$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );

		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Add in conditional search filters for products.
	 *
	 * @internal
	 * @param string $where Where clause used to search posts.
	 * @param object $wp_query WP_Query object.
	 * @return string
	 */
	public static function add_wp_query_filter( $where, $wp_query ) {
		global $wpdb;

		$stock_status = $wp_query->get( 'stock_status' );
		if ( $stock_status ) {
			$where .= $wpdb->prepare(
				' AND wc_product_meta_lookup.stock_status = %s ',
				$stock_status
			);
		}

		if ( $wp_query->get( 'low_in_stock' ) ) {
			// We want products with stock < low stock amount, but greater than no stock amount.
			$no_stock_amount  = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) );
			$low_stock_amount = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
			$where           .= "
			AND wc_product_meta_lookup.stock_quantity IS NOT NULL
			AND wc_product_meta_lookup.stock_status = 'instock'
			AND (
				(
					low_stock_amount_meta.meta_value > ''
					AND wc_product_meta_lookup.stock_quantity <= CAST(low_stock_amount_meta.meta_value AS SIGNED)
					AND wc_product_meta_lookup.stock_quantity > {$no_stock_amount}
				)
				OR (
					(
						low_stock_amount_meta.meta_value IS NULL OR low_stock_amount_meta.meta_value <= ''
					)
					AND wc_product_meta_lookup.stock_quantity <= {$low_stock_amount}
					AND wc_product_meta_lookup.stock_quantity > {$no_stock_amount}
				)
			)";
		}

		return $where;
	}

	/**
	 * Join posts meta tables when product search or low stock query is present.
	 *
	 * @internal
	 * @param string $join Join clause used to search posts.
	 * @param object $wp_query WP_Query object.
	 * @return string
	 */
	public static function add_wp_query_join( $join, $wp_query ) {
		global $wpdb;

		$stock_status = $wp_query->get( 'stock_status' );
		if ( $stock_status ) {
			$join = self::append_product_sorting_table_join( $join );
		}

		if ( $wp_query->get( 'low_in_stock' ) ) {
			$join  = self::append_product_sorting_table_join( $join );
			$join .= " LEFT JOIN {$wpdb->postmeta} AS low_stock_amount_meta ON {$wpdb->posts}.ID = low_stock_amount_meta.post_id AND low_stock_amount_meta.meta_key = '_low_stock_amount' ";
		}

		return $join;
	}

	/**
	 * Join wc_product_meta_lookup to posts if not already joined.
	 *
	 * @internal
	 * @param string $sql SQL join.
	 * @return string
	 */
	protected static function append_product_sorting_table_join( $sql ) {
		global $wpdb;

		if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) {
			$sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
		}
		return $sql;
	}

	/**
	 * Group by post ID to prevent duplicates.
	 *
	 * @internal
	 * @param string $groupby Group by clause used to organize posts.
	 * @param object $wp_query WP_Query object.
	 * @return string
	 */
	public static function add_wp_query_group_by( $groupby, $wp_query ) {
		global $wpdb;

		if ( empty( $groupby ) ) {
			$groupby = $wpdb->posts . '.ID';
		}
		return $groupby;
	}

	/**
	 * Custom orderby clauses using the lookup tables.
	 *
	 * @internal
	 * @param array  $args Query args.
	 * @param object $wp_query WP_Query object.
	 * @return array
	 */
	public static function add_wp_query_orderby( $args, $wp_query ) {
		global $wpdb;

		$orderby = $wp_query->get( 'orderby' );
		$order   = esc_sql( $wp_query->get( 'order' ) ? $wp_query->get( 'order' ) : 'desc' );

		switch ( $orderby ) {
			case 'stock_quantity':
				$args['join']    = self::append_product_sorting_table_join( $args['join'] );
				$args['orderby'] = " wc_product_meta_lookup.stock_quantity {$order}, wc_product_meta_lookup.product_id {$order} ";
				break;
			case 'stock_status':
				$args['join']    = self::append_product_sorting_table_join( $args['join'] );
				$args['orderby'] = " wc_product_meta_lookup.stock_status {$order}, wc_product_meta_lookup.stock_quantity {$order} ";
				break;
			case 'sku':
				$args['join']    = self::append_product_sorting_table_join( $args['join'] );
				$args['orderby'] = " wc_product_meta_lookup.sku {$order}, wc_product_meta_lookup.product_id {$order} ";
				break;
		}

		return $args;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param  WC_Product      $product  Report data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $product, $request ) {
		$data = array(
			'id'               => $product->get_id(),
			'parent_id'        => $product->get_parent_id(),
			'name'             => wp_strip_all_tags( $product->get_name() ),
			'sku'              => $product->get_sku(),
			'stock_status'     => $product->get_stock_status(),
			'stock_quantity'   => (float) $product->get_stock_quantity(),
			'manage_stock'     => $product->get_manage_stock(),
			'low_stock_amount' => $product->get_low_stock_amount(),
		);

		if ( '' === $data['low_stock_amount'] ) {
			$data['low_stock_amount'] = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $product ) );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WC_Product       $product   The original product object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_stock', $response, $product, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param  WC_Product $product Object data.
	 * @return array
	 */
	protected function prepare_links( $product ) {
		if ( $product->is_type( 'variation' ) ) {
			$links = array(
				'product' => array(
					'href' => rest_url( sprintf( '/%s/products/%d/variations/%d', $this->namespace, $product->get_parent_id(), $product->get_id() ) ),
				),
				'parent'  => array(
					'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_parent_id() ) ),
				),
			);
		} elseif ( $product->get_parent_id() ) {
			$links = array(
				'product' => array(
					'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_id() ) ),
				),
				'parent'  => array(
					'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_parent_id() ) ),
				),
			);
		} else {
			$links = array(
				'product' => array(
					'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_id() ) ),
				),
			);
		}

		return $links;
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_stock',
			'type'       => 'object',
			'properties' => array(
				'id'             => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'parent_id'      => array(
					'description' => __( 'Product parent ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'           => array(
					'description' => __( 'Product name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'sku'            => array(
					'description' => __( 'Unique identifier.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'stock_status'   => array(
					'description' => __( 'Stock status.', 'woocommerce' ),
					'type'        => 'string',
					'enum'        => array_keys( wc_get_product_stock_status_options() ),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'stock_quantity' => array(
					'description' => __( 'Stock quantity.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'manage_stock'   => array(
					'description' => __( 'Manage stock.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                   = array();
		$params['context']        = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']           = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']       = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['exclude']        = array(
			'description'       => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['include']        = array(
			'description'       => __( 'Limit result set to specific ids.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['offset']         = array(
			'description'       => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']          = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'asc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']        = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'stock_status',
			'enum'              => array(
				'stock_status',
				'stock_quantity',
				'date',
				'id',
				'include',
				'title',
				'sku',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['parent']         = array(
			'description'       => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'sanitize_callback' => 'wp_parse_id_list',
			'default'           => array(),
		);
		$params['parent_exclude'] = array(
			'description'       => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'sanitize_callback' => 'wp_parse_id_list',
			'default'           => array(),
		);
		$params['type']           = array(
			'description' => __( 'Limit result set to items assigned a stock report type.', 'woocommerce' ),
			'type'        => 'string',
			'default'     => 'all',
			'enum'        => array_merge( array( 'all', 'lowstock' ), array_keys( wc_get_product_stock_status_options() ) ),
		);

		return $params;
	}

	/**
	 * Get the column names for export.
	 *
	 * @return array Key value pair of Column ID => Label.
	 */
	public function get_export_columns() {
		$export_columns = array(
			'title'          => __( 'Product / Variation', 'woocommerce' ),
			'sku'            => __( 'SKU', 'woocommerce' ),
			'stock_status'   => __( 'Status', 'woocommerce' ),
			'stock_quantity' => __( 'Stock', 'woocommerce' ),
		);

		/**
		 * Filter to add or remove column names from the stock report for
		 * export.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_report_stock_export_columns',
			$export_columns
		);
	}

	/**
	 * Get the column values for export.
	 *
	 * @param array $item Single report item/row.
	 * @return array Key value pair of Column ID => Row Value.
	 */
	public function prepare_item_for_export( $item ) {
		$status = $item['stock_status'];
		if ( array_key_exists( $item['stock_status'], $this->status_options ) ) {
			$status = $this->status_options[ $item['stock_status'] ];
		}

		$export_item = array(
			'title'          => $item['name'],
			'sku'            => $item['sku'],
			'stock_status'   => $status,
			'stock_quantity' => $item['stock_quantity'],
		);

		/**
		 * Filter to prepare extra columns in the export item for the stock
		 * report.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_report_stock_prepare_export_item',
			$export_item,
			$item
		);
	}
}
Admin/API/Reports/Cache.php000064400000002753152333011040011356 0ustar00<?php
/**
 * REST API Reports Cache.
 *
 * Handles report data object caching.
 */

namespace Automattic\WooCommerce\Admin\API\Reports;

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports Cache class.
 */
class Cache {
	/**
	 * Cache version. Used to invalidate all cached values.
	 */
	const VERSION_OPTION = 'woocommerce_reports';

	/**
	 * Invalidate cache.
	 */
	public static function invalidate() {
		\WC_Cache_Helper::get_transient_version( self::VERSION_OPTION, true );
	}

	/**
	 * Get cache version number.
	 *
	 * @return string
	 */
	public static function get_version() {
		$version = \WC_Cache_Helper::get_transient_version( self::VERSION_OPTION );

		return $version;
	}

	/**
	 * Get cached value.
	 *
	 * @param string $key Cache key.
	 * @return mixed
	 */
	public static function get( $key ) {
		$transient_version = self::get_version();
		$transient_value   = get_transient( $key );

		if (
			isset( $transient_value['value'], $transient_value['version'] ) &&
			$transient_value['version'] === $transient_version
		) {
			return $transient_value['value'];
		}

		return false;
	}

	/**
	 * Update cached value.
	 *
	 * @param string $key   Cache key.
	 * @param mixed  $value New value.
	 * @return bool
	 */
	public static function set( $key, $value ) {
		$transient_version = self::get_version();
		$transient_value   = array(
			'version' => $transient_version,
			'value'   => $value,
		);

		$result = set_transient( $key, $transient_value, WEEK_IN_SECONDS );

		return $result;
	}
}
Admin/API/Reports/ParameterException.php000064400000000506152333011040014144 0ustar00<?php
/**
 * WooCommerce Admin Input Parameter Exception Class
 *
 * Exception class thrown when user provides incorrect parameters.
 */

namespace Automattic\WooCommerce\Admin\API\Reports;

defined( 'ABSPATH' ) || exit;

/**
 * API\Reports\ParameterException class.
 */
class ParameterException extends \WC_Data_Exception {}
Admin/API/Reports/Categories/DataStore.php000064400000025221152333011040014321 0ustar00<?php
/**
 * API\Reports\Categories\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Categories;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;

/**
 * API\Reports\Categories\DataStore.
 */
class DataStore extends ReportsDataStore implements DataStoreInterface {

	/**
	 * Table used to get the data.
	 *
	 * @var string
	 */
	protected static $table_name = 'wc_order_product_lookup';

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'categories';

	/**
	 * Order by setting used for sorting categories data.
	 *
	 * @var string
	 */
	private $order_by = '';

	/**
	 * Order setting used for sorting categories data.
	 *
	 * @var string
	 */
	private $order = '';

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'category_id'    => 'intval',
		'items_sold'     => 'intval',
		'net_revenue'    => 'floatval',
		'orders_count'   => 'intval',
		'products_count' => 'intval',
	);

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'categories';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$table_name           = self::get_db_table_name();
		$this->report_columns = array(
			'items_sold'     => 'SUM(product_qty) as items_sold',
			'net_revenue'    => 'SUM(product_net_revenue) AS net_revenue',
			'orders_count'   => "COUNT(DISTINCT {$table_name}.order_id) as orders_count",
			'products_count' => "COUNT(DISTINCT {$table_name}.product_id) as products_count",
		);
	}

	/**
	 * Return the database query with parameters used for Categories report: time span and order status.
	 *
	 * @param array $query_args Query arguments supplied by the user.
	 */
	protected function add_sql_query_params( $query_args ) {
		global $wpdb;
		$order_product_lookup_table = self::get_db_table_name();

		$this->add_time_period_sql_params( $query_args, $order_product_lookup_table );

		// join wp_order_product_lookup_table with relationships and taxonomies
		// @todo How to handle custom product tables?
		$this->subquery->add_sql_clause( 'left_join', "LEFT JOIN {$wpdb->term_relationships} ON {$order_product_lookup_table}.product_id = {$wpdb->term_relationships}.object_id" );
		// Adding this (inner) JOIN as a LEFT JOIN for ordering purposes. See comment in add_order_by_params().
		$this->subquery->add_sql_clause( 'left_join', "JOIN {$wpdb->term_taxonomy} ON {$wpdb->term_taxonomy}.term_taxonomy_id = {$wpdb->term_relationships}.term_taxonomy_id" );

		$included_categories = $this->get_included_categories( $query_args );
		if ( $included_categories ) {
			$this->subquery->add_sql_clause( 'where', "AND {$wpdb->term_relationships}.term_taxonomy_id IN ({$included_categories})" );

			// Limit is left out here so that the grouping in code by PHP can be applied correctly.
			// This also needs to be put after the term_taxonomy JOIN so that we can match the correct term name.
			$this->add_order_by_params( $query_args, 'outer', 'default_results.category_id' );
		} else {
			$this->add_order_by_params( $query_args, 'inner', "{$wpdb->term_relationships}.term_taxonomy_id" );
		}

		$this->add_order_status_clause( $query_args, $order_product_lookup_table, $this->subquery );
		$this->subquery->add_sql_clause( 'where', "AND {$wpdb->term_taxonomy}.taxonomy = 'product_cat'" );
	}

	/**
	 * Fills ORDER BY clause of SQL request based on user supplied parameters.
	 *
	 * @param array  $query_args Parameters supplied by the user.
	 * @param string $from_arg   Target of the JOIN sql param.
	 * @param string $id_cell    ID cell identifier, like `table_name.id_column_name`.
	 */
	protected function add_order_by_params( $query_args, $from_arg, $id_cell ) {
		global $wpdb;

		// Sanitize input: guarantee that the id cell in the join is quoted with backticks.
		$id_cell_segments   = explode( '.', str_replace( '`', '', $id_cell ) );
		$id_cell_identifier = '`' . implode( '`.`', $id_cell_segments ) . '`';

		$lookup_table    = self::get_db_table_name();
		$order_by_clause = $this->add_order_by_clause( $query_args, $this );
		$this->add_orderby_order_clause( $query_args, $this );

		if ( false !== strpos( $order_by_clause, '_terms' ) ) {
			$join = "JOIN {$wpdb->terms} AS _terms ON {$id_cell_identifier} = _terms.term_id";
			if ( 'inner' === $from_arg ) {
				// Even though this is an (inner) JOIN, we're adding it as a `left_join` to
				// affect its order in the query statement. The SqlQuery::$sql_filters variable
				// determines the order in which joins are concatenated.
				// See: https://github.com/woocommerce/woocommerce-admin/blob/1f261998e7287b77bc13c3d4ee2e84b717da7957/src/API/Reports/SqlQuery.php#L46-L50.
				$this->subquery->add_sql_clause( 'left_join', $join );
			} else {
				$this->add_sql_clause( 'join', $join );
			}
		}
	}

	/**
	 * Maps ordering specified by the user to columns in the database/fields in the data.
	 *
	 * @param string $order_by Sorting criterion.
	 * @return string
	 */
	protected function normalize_order_by( $order_by ) {
		if ( 'date' === $order_by ) {
			return 'time_interval';
		}
		if ( 'category' === $order_by ) {
			return '_terms.name';
		}
		return $order_by;
	}

	/**
	 * Returns an array of ids of included categories, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_included_categories_array( $query_args ) {
		if ( isset( $query_args['category_includes'] ) && is_array( $query_args['category_includes'] ) && count( $query_args['category_includes'] ) > 0 ) {
			return $query_args['category_includes'];
		}
		return array();
	}

	/**
	 * Returns the page of data according to page number and items per page.
	 *
	 * @param array   $data           Data to paginate.
	 * @param integer $page_no        Page number.
	 * @param integer $items_per_page Number of items per page.
	 * @return array
	 */
	protected function page_records( $data, $page_no, $items_per_page ) {
		$offset = ( $page_no - 1 ) * $items_per_page;
		return array_slice( $data, $offset, $items_per_page );
	}

	/**
	 * Enriches the category data.
	 *
	 * @param array $categories_data Categories data.
	 * @param array $query_args  Query parameters.
	 */
	protected function include_extended_info( &$categories_data, $query_args ) {
		foreach ( $categories_data as $key => $category_data ) {
			$extended_info = new \ArrayObject();
			if ( $query_args['extended_info'] ) {
				$extended_info['name'] = get_the_category_by_ID( $category_data['category_id'] );
			}
			$categories_data[ $key ]['extended_info'] = $extended_info;
		}
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page'          => get_option( 'posts_per_page' ),
			'page'              => 1,
			'order'             => 'DESC',
			'orderby'           => 'date',
			'before'            => TimeInterval::default_before(),
			'after'             => TimeInterval::default_after(),
			'fields'            => '*',
			'category_includes' => array(),
			'extended_info'     => false,
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$data = (object) array(
				'data'    => array(),
				'total'   => 0,
				'pages'   => 0,
				'page_no' => 0,
			);

			$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
			$included_categories = $this->get_included_categories_array( $query_args );
			$this->add_sql_query_params( $query_args );

			if ( count( $included_categories ) > 0 ) {
				$fields    = $this->get_fields( $query_args );
				$ids_table = $this->get_ids_table( $included_categories, 'category_id' );

				$this->add_sql_clause( 'select', $this->format_join_selections( array_merge( array( 'category_id' ), $fields ), array( 'category_id' ) ) );
				$this->add_sql_clause( 'from', '(' );
				$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
				$this->add_sql_clause( 'from', ") AS {$table_name}" );
				$this->add_sql_clause(
					'right_join',
					"RIGHT JOIN ( {$ids_table} ) AS default_results
					ON default_results.category_id = {$table_name}.category_id"
				);

				$categories_query = $this->get_query_statement();
			} else {
				$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
				$categories_query = $this->subquery->get_query_statement();
			}
			$categories_data = $wpdb->get_results(
				$categories_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				ARRAY_A
			);

			if ( null === $categories_data ) {
				return new \WP_Error( 'woocommerce_analytics_categories_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ), array( 'status' => 500 ) );
			}

			$record_count = count( $categories_data );
			$total_pages  = (int) ceil( $record_count / $query_args['per_page'] );
			if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
				return $data;
			}

			$categories_data = $this->page_records( $categories_data, $query_args['page'], $query_args['per_page'] );
			$this->include_extended_info( $categories_data, $query_args );
			$categories_data = array_map( array( $this, 'cast_numbers' ), $categories_data );
			$data            = (object) array(
				'data'    => $categories_data,
				'total'   => $record_count,
				'pages'   => $total_pages,
				'page_no' => (int) $query_args['page'],
			);

			$this->set_cached_data( $cache_key, $data );
		}

		return $data;
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		global $wpdb;
		$this->subquery = new SqlQuery( $this->context . '_subquery' );
		$this->subquery->add_sql_clause( 'select', "{$wpdb->term_taxonomy}.term_id as category_id," );
		$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
		$this->subquery->add_sql_clause( 'group_by', "{$wpdb->term_taxonomy}.term_id" );
	}
}
Admin/API/Reports/Categories/Query.php000064400000002370152333011040013540 0ustar00<?php
/**
 * Class for parameter-based Categories Report querying
 *
 * Example usage:
 * $args = array(
 *          'before'       => '2018-07-19 00:00:00',
 *          'after'        => '2018-07-05 00:00:00',
 *          'page'         => 2,
 *          'order'        => 'desc',
 *          'orderby'      => 'items_sold',
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Categories\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Categories;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Query
 */
class Query extends ReportsQuery {

	const REPORT_NAME = 'report-categories';

	/**
	 * Valid fields for Categories report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array();
	}

	/**
	 * Get categories data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args    = apply_filters( 'woocommerce_analytics_categories_query_args', $this->get_query_vars() );
		$results = \WC_Data_Store::load( self::REPORT_NAME )->get_data( $args );
		return apply_filters( 'woocommerce_analytics_categories_select_query', $results, $args );
	}
}
Admin/API/Reports/Categories/Controller.php000064400000030131152333011040014552 0ustar00<?php
/**
 * REST API Reports categories controller
 *
 * Handles requests to the /reports/categories endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Categories;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;

/**
 * REST API Reports categories controller class.
 *
 * @internal
 * @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
 */
class Controller extends ReportsController implements ExportableInterface {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/categories';

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args                        = array();
		$args['before']              = $request['before'];
		$args['after']               = $request['after'];
		$args['interval']            = $request['interval'];
		$args['page']                = $request['page'];
		$args['per_page']            = $request['per_page'];
		$args['orderby']             = $request['orderby'];
		$args['order']               = $request['order'];
		$args['extended_info']       = $request['extended_info'];
		$args['category_includes']   = (array) $request['categories'];
		$args['status_is']           = (array) $request['status_is'];
		$args['status_is_not']       = (array) $request['status_is_not'];
		$args['force_cache_refresh'] = $request['force_cache_refresh'];

		return $args;
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$query_args       = $this->prepare_reports_query( $request );
		$categories_query = new Query( $query_args );
		$report_data      = $categories_query->get_data();

		if ( is_wp_error( $report_data ) ) {
			return $report_data;
		}

		if ( ! isset( $report_data->data ) || ! isset( $report_data->page_no ) || ! isset( $report_data->pages ) ) {
			return new \WP_Error( 'woocommerce_rest_reports_categories_invalid_response', __( 'Invalid response from data store.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		$out_data = array();

		foreach ( $report_data->data as $datum ) {
			$item       = $this->prepare_item_for_response( $datum, $request );
			$out_data[] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $out_data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		$page      = $report_data->page_no;
		$max_pages = $report_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param stdClass        $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $report ) );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_categories', $response, $report, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param \Automattic\WooCommerce\Admin\API\Reports\Query $object Object data.
	 * @return array
	 */
	protected function prepare_links( $object ) {
		$links = array(
			'category' => array(
				'href' => rest_url( sprintf( '/%s/products/categories/%d', $this->namespace, $object['category_id'] ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_categories',
			'type'       => 'object',
			'properties' => array(
				'category_id'    => array(
					'description' => __( 'Category ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'items_sold'     => array(
					'description' => __( 'Amount of items sold.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'net_revenue'    => array(
					'description' => __( 'Total sales.', 'woocommerce' ),
					'type'        => 'number',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'orders_count'   => array(
					'description' => __( 'Number of orders.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'products_count' => array(
					'description' => __( 'Amount of products.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'extended_info'  => array(
					'name' => array(
						'type'        => 'string',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Category name.', 'woocommerce' ),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                  = array();
		$params['context']       = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']          = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']      = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']         = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']        = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']         = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']       = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'category_id',
			'enum'              => array(
				'category_id',
				'items_sold',
				'net_revenue',
				'orders_count',
				'products_count',
				'category',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['interval']      = array(
			'description'       => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'week',
			'enum'              => array(
				'hour',
				'day',
				'week',
				'month',
				'quarter',
				'year',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['status_is']     = array(
			'description'       => __( 'Limit result set to items that have the specified order status.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'enum' => self::get_order_statuses(),
				'type' => 'string',
			),
		);
		$params['status_is_not'] = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified order status.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'enum' => self::get_order_statuses(),
				'type' => 'string',
			),
		);
		$params['categories']    = array(
			'description'       => __( 'Limit result set to all items that have the specified term assigned in the categories taxonomy.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['extended_info'] = array(
			'description'       => __( 'Add additional piece of info about each category to the report.', 'woocommerce' ),
			'type'              => 'boolean',
			'default'           => false,
			'sanitize_callback' => 'wc_string_to_bool',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get the column names for export.
	 *
	 * @return array Key value pair of Column ID => Label.
	 */
	public function get_export_columns() {
		$export_columns = array(
			'category'       => __( 'Category', 'woocommerce' ),
			'items_sold'     => __( 'Items sold', 'woocommerce' ),
			'net_revenue'    => __( 'Net Revenue', 'woocommerce' ),
			'products_count' => __( 'Products', 'woocommerce' ),
			'orders_count'   => __( 'Orders', 'woocommerce' ),
		);

		/**
		 * Filter to add or remove column names from the categories report for
		 * export.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_report_categories_export_columns',
			$export_columns
		);
	}

	/**
	 * Get the column values for export.
	 *
	 * @param array $item Single report item/row.
	 * @return array Key value pair of Column ID => Row Value.
	 */
	public function prepare_item_for_export( $item ) {
		$export_item = array(
			'category'       => $item['extended_info']['name'],
			'items_sold'     => $item['items_sold'],
			'net_revenue'    => $item['net_revenue'],
			'products_count' => $item['products_count'],
			'orders_count'   => $item['orders_count'],
		);

		/**
		 * Filter to prepare extra columns in the export item for the
		 * categories export.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_report_categories_prepare_export_item',
			$export_item,
			$item
		);
	}
}
Admin/API/Reports/Segmenter.php000064400000052326152333011040012305 0ustar00<?php
/**
 * Class for adding segmenting support without cluttering the data stores.
 */

namespace Automattic\WooCommerce\Admin\API\Reports;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\DataStore as TaxesStatsDataStore;

/**
 * Date & time interval and numeric range handling class for Reporting API.
 */
class Segmenter {

	/**
	 * Array of all segment ids.
	 *
	 * @var array|bool
	 */
	protected $all_segment_ids = false;

	/**
	 * Array of all segment labels.
	 *
	 * @var array
	 */
	protected $segment_labels = array();

	/**
	 * Query arguments supplied by the user for data store.
	 *
	 * @var array
	 */
	protected $query_args = '';

	/**
	 * SQL definition for each column.
	 *
	 * @var array
	 */
	protected $report_columns = array();

	/**
	 * Constructor.
	 *
	 * @param array $query_args Query arguments supplied by the user for data store.
	 * @param array $report_columns Report columns lookup from data store.
	 */
	public function __construct( $query_args, $report_columns ) {
		$this->query_args     = $query_args;
		$this->report_columns = $report_columns;
	}

	/**
	 * Filters definitions for SELECT clauses based on query_args and joins them into one string usable in SELECT clause.
	 *
	 * @param array $columns_mapping Column name -> SQL statememt mapping.
	 *
	 * @return string to be used in SELECT clause statements.
	 */
	protected function prepare_selections( $columns_mapping ) {
		if ( isset( $this->query_args['fields'] ) && is_array( $this->query_args['fields'] ) ) {
			$keep = array();
			foreach ( $this->query_args['fields'] as $field ) {
				if ( isset( $columns_mapping[ $field ] ) ) {
					$keep[ $field ] = $columns_mapping[ $field ];
				}
			}
			$selections = implode( ', ', $keep );
		} else {
			$selections = implode( ', ', $columns_mapping );
		}

		if ( $selections ) {
			$selections = ',' . $selections;
		}

		return $selections;
	}

	/**
	 * Update row-level db result for segments in 'totals' section to the format used for output.
	 *
	 * @param array  $segments_db_result Results from the SQL db query for segmenting.
	 * @param string $segment_dimension Name of column used for grouping the result.
	 *
	 * @return array Reformatted array.
	 */
	protected function reformat_totals_segments( $segments_db_result, $segment_dimension ) {
		$segment_result = array();

		if ( strpos( $segment_dimension, '.' ) ) {
			$segment_dimension = substr( strstr( $segment_dimension, '.' ), 1 );
		}

		$segment_labels = $this->get_segment_labels();
		foreach ( $segments_db_result as $segment_data ) {
			$segment_id = $segment_data[ $segment_dimension ];
			if ( ! isset( $segment_labels[ $segment_id ] ) ) {
				continue;
			}

			unset( $segment_data[ $segment_dimension ] );
			$segment_datum                 = array(
				'segment_id'    => $segment_id,
				'segment_label' => $segment_labels[ $segment_id ],
				'subtotals'     => $segment_data,
			);
			$segment_result[ $segment_id ] = $segment_datum;
		}

		return $segment_result;
	}

	/**
	 * Merges segmented results for totals response part.
	 *
	 * E.g. $r1 = array(
	 *     0 => array(
	 *          'product_id' => 3,
	 *          'net_amount' => 15,
	 *     ),
	 * );
	 * $r2 = array(
	 *     0 => array(
	 *          'product_id'      => 3,
	 *          'avg_order_value' => 25,
	 *     ),
	 * );
	 *
	 * $merged = array(
	 *     3 => array(
	 *          'segment_id' => 3,
	 *          'subtotals'  => array(
	 *              'net_amount'      => 15,
	 *              'avg_order_value' => 25,
	 *          )
	 *     ),
	 * );
	 *
	 * @param string $segment_dimension Name of the segment dimension=key in the result arrays used to match records from result sets.
	 * @param array  $result1 Array 1 of segmented figures.
	 * @param array  $result2 Array 2 of segmented figures.
	 *
	 * @return array
	 */
	protected function merge_segment_totals_results( $segment_dimension, $result1, $result2 ) {
		$result_segments = array();
		$segment_labels  = $this->get_segment_labels();

		foreach ( $result1 as $segment_data ) {
			$segment_id = $segment_data[ $segment_dimension ];
			if ( ! isset( $segment_labels[ $segment_id ] ) ) {
				continue;
			}

			unset( $segment_data[ $segment_dimension ] );
			$result_segments[ $segment_id ] = array(
				'segment_label' => $segment_labels[ $segment_id ],
				'segment_id'    => $segment_id,
				'subtotals'     => $segment_data,
			);
		}

		foreach ( $result2 as $segment_data ) {
			$segment_id = $segment_data[ $segment_dimension ];
			if ( ! isset( $segment_labels[ $segment_id ] ) ) {
				continue;
			}

			unset( $segment_data[ $segment_dimension ] );
			if ( ! isset( $result_segments[ $segment_id ] ) ) {
				$result_segments[ $segment_id ] = array(
					'segment_label' => $segment_labels[ $segment_id ],
					'segment_id'    => $segment_id,
					'subtotals'     => array(),
				);
			}
			$result_segments[ $segment_id ]['subtotals'] = array_merge( $result_segments[ $segment_id ]['subtotals'], $segment_data );
		}
		return $result_segments;
	}
	/**
	 * Merges segmented results for intervals response part.
	 *
	 * E.g. $r1 = array(
	 *     0 => array(
	 *          'product_id'    => 3,
	 *          'time_interval' => '2018-12'
	 *          'net_amount'    => 15,
	 *     ),
	 * );
	 * $r2 = array(
	 *     0 => array(
	 *          'product_id'      => 3,
	 *          'time_interval' => '2018-12'
	 *          'avg_order_value' => 25,
	 *     ),
	 * );
	 *
	 * $merged = array(
	 *     '2018-12' => array(
	 *          'segments' => array(
	 *              3 => array(
	 *                  'segment_id' => 3,
	 *                  'subtotals'  => array(
	 *                      'net_amount'      => 15,
	 *                      'avg_order_value' => 25,
	 *                  ),
	 *              ),
	 *          ),
	 *     ),
	 * );
	 *
	 * @param string $segment_dimension Name of the segment dimension=key in the result arrays used to match records from result sets.
	 * @param array  $result1 Array 1 of segmented figures.
	 * @param array  $result2 Array 2 of segmented figures.
	 *
	 * @return array
	 */
	protected function merge_segment_intervals_results( $segment_dimension, $result1, $result2 ) {
		$result_segments = array();
		$segment_labels  = $this->get_segment_labels();

		foreach ( $result1 as $segment_data ) {
			$segment_id = $segment_data[ $segment_dimension ];
			if ( ! isset( $segment_labels[ $segment_id ] ) ) {
				continue;
			}

			$time_interval = $segment_data['time_interval'];
			if ( ! isset( $result_segments[ $time_interval ] ) ) {
				$result_segments[ $time_interval ]             = array();
				$result_segments[ $time_interval ]['segments'] = array();
			}

			unset( $segment_data['time_interval'] );
			unset( $segment_data['datetime_anchor'] );
			unset( $segment_data[ $segment_dimension ] );
			$segment_datum = array(
				'segment_label' => $segment_labels[ $segment_id ],
				'segment_id'    => $segment_id,
				'subtotals'     => $segment_data,
			);
			$result_segments[ $time_interval ]['segments'][ $segment_id ] = $segment_datum;
		}

		foreach ( $result2 as $segment_data ) {
			$segment_id = $segment_data[ $segment_dimension ];
			if ( ! isset( $segment_labels[ $segment_id ] ) ) {
				continue;
			}

			$time_interval = $segment_data['time_interval'];
			if ( ! isset( $result_segments[ $time_interval ] ) ) {
				$result_segments[ $time_interval ]             = array();
				$result_segments[ $time_interval ]['segments'] = array();
			}

			unset( $segment_data['time_interval'] );
			unset( $segment_data['datetime_anchor'] );
			unset( $segment_data[ $segment_dimension ] );

			if ( ! isset( $result_segments[ $time_interval ]['segments'][ $segment_id ] ) ) {
				$result_segments[ $time_interval ]['segments'][ $segment_id ] = array(
					'segment_label' => $segment_labels[ $segment_id ],
					'segment_id'    => $segment_id,
					'subtotals'     => array(),
				);
			}
			$result_segments[ $time_interval ]['segments'][ $segment_id ]['subtotals'] = array_merge( $result_segments[ $time_interval ]['segments'][ $segment_id ]['subtotals'], $segment_data );
		}
		return $result_segments;
	}

	/**
	 * Update row-level db result for segments in 'intervals' section to the format used for output.
	 *
	 * @param array  $segments_db_result Results from the SQL db query for segmenting.
	 * @param string $segment_dimension Name of column used for grouping the result.
	 *
	 * @return array Reformatted array.
	 */
	protected function reformat_intervals_segments( $segments_db_result, $segment_dimension ) {
		$aggregated_segment_result = array();

		if ( strpos( $segment_dimension, '.' ) ) {
			$segment_dimension = substr( strstr( $segment_dimension, '.' ), 1 );
		}

		$segment_labels = $this->get_segment_labels();

		foreach ( $segments_db_result as $segment_data ) {
			$segment_id = $segment_data[ $segment_dimension ];
			if ( ! isset( $segment_labels[ $segment_id ] ) ) {
				continue;
			}

			$time_interval = $segment_data['time_interval'];
			if ( ! isset( $aggregated_segment_result[ $time_interval ] ) ) {
				$aggregated_segment_result[ $time_interval ]             = array();
				$aggregated_segment_result[ $time_interval ]['segments'] = array();
			}
			unset( $segment_data['time_interval'] );
			unset( $segment_data['datetime_anchor'] );
			unset( $segment_data[ $segment_dimension ] );
			$segment_datum = array(
				'segment_label' => $segment_labels[ $segment_id ],
				'segment_id'    => $segment_id,
				'subtotals'     => $segment_data,
			);
			$aggregated_segment_result[ $time_interval ]['segments'][ $segment_id ] = $segment_datum;
		}

		return $aggregated_segment_result;
	}

	/**
	 * Fetches all segment ids from db and stores it for later use.
	 *
	 * @return void
	 */
	protected function set_all_segments() {
		global $wpdb;

		if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
			$this->all_segment_ids = array();
			return;
		}

		$segments       = array();
		$segment_labels = array();

		if ( 'product' === $this->query_args['segmentby'] ) {
			$args = array(
				'return' => 'objects',
				'limit'  => -1,
			);

			if ( isset( $this->query_args['product_includes'] ) ) {
				$args['include'] = $this->query_args['product_includes'];
			}

			if ( isset( $this->query_args['category_includes'] ) ) {
				$categories       = $this->query_args['category_includes'];
				$args['category'] = array();
				foreach ( $categories as $category_id ) {
					$terms              = get_term_by( 'id', $category_id, 'product_cat' );
					$args['category'][] = $terms->slug;
				}
			}

			$segment_objects = wc_get_products( $args );
			foreach ( $segment_objects as $segment ) {
				$id                    = $segment->get_id();
				$segments[]            = $id;
				$segment_labels[ $id ] = $segment->get_name();
			}
		} elseif ( 'variation' === $this->query_args['segmentby'] ) {
			$args = array(
				'return' => 'objects',
				'limit'  => -1,
				'type'   => 'variation',
			);

			if (
				isset( $this->query_args['product_includes'] ) &&
				count( $this->query_args['product_includes'] ) === 1
			) {
				$args['parent'] = $this->query_args['product_includes'][0];
			}

			if ( isset( $this->query_args['variation_includes'] ) ) {
				$args['include'] = $this->query_args['variation_includes'];
			}

			$segment_objects = wc_get_products( $args );

			foreach ( $segment_objects as $segment ) {
				$id           = $segment->get_id();
				$segments[]   = $id;
				$product_name = $segment->get_name();
				$separator    = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $segment );
				$attributes   = wc_get_formatted_variation( $segment, true, false );

				$segment_labels[ $id ] = $product_name . $separator . $attributes;
			}

			// If no variations were specified, add a segment for the parent product (variation = 0).
			// This is to catch simple products with prior sales converted into variable products.
			// See: https://github.com/woocommerce/woocommerce-admin/issues/2719.
			if ( isset( $args['parent'] ) && empty( $args['include'] ) ) {
				$parent_object     = wc_get_product( $args['parent'] );
				$segments[]        = 0;
				$segment_labels[0] = $parent_object->get_name();
			}
		} elseif ( 'category' === $this->query_args['segmentby'] ) {
			$args = array(
				'taxonomy' => 'product_cat',
			);

			if ( isset( $this->query_args['category_includes'] ) ) {
				$args['include'] = $this->query_args['category_includes'];
			}

			// @todo: Look into `wc_get_products` or data store methods and not directly touching the database or post types.
			$categories = get_categories( $args );

			$segments       = wp_list_pluck( $categories, 'cat_ID' );
			$segment_labels = wp_list_pluck( $categories, 'name', 'cat_ID' );

		} elseif ( 'coupon' === $this->query_args['segmentby'] ) {
			$args = array();
			if ( isset( $this->query_args['coupons'] ) ) {
				$args['include'] = $this->query_args['coupons'];
			}
			$coupons_store  = new CouponsDataStore();
			$coupons        = $coupons_store->get_coupons( $args );
			$segments       = wp_list_pluck( $coupons, 'ID' );
			$segment_labels = wp_list_pluck( $coupons, 'post_title', 'ID' );
			$segment_labels = array_map( 'wc_format_coupon_code', $segment_labels );
		} elseif ( 'customer_type' === $this->query_args['segmentby'] ) {
			// 0 -- new customer
			// 1 -- returning customer
			$segments = array( 0, 1 );
		} elseif ( 'tax_rate_id' === $this->query_args['segmentby'] ) {
			$args = array();
			if ( isset( $this->query_args['taxes'] ) ) {
				$args['include'] = $this->query_args['taxes'];
			}
			$taxes = TaxesStatsDataStore::get_taxes( $args );

			foreach ( $taxes as $tax ) {
				$id                    = $tax['tax_rate_id'];
				$segments[]            = $id;
				$segment_labels[ $id ] = \WC_Tax::get_rate_code( (object) $tax );
			}
		} else {
			// Catch all default.
			$segments = array();
		}

		$this->all_segment_ids = $segments;
		$this->segment_labels  = $segment_labels;
	}

	/**
	 * Return all segment ids for given segmentby query parameter.
	 *
	 * @return array
	 */
	protected function get_all_segments() {
		if ( ! is_array( $this->all_segment_ids ) ) {
			$this->set_all_segments();
		}

		return $this->all_segment_ids;
	}

	/**
	 * Return all segment labels for given segmentby query parameter.
	 *
	 * @return array
	 */
	protected function get_segment_labels() {
		if ( ! is_array( $this->all_segment_ids ) ) {
			$this->set_all_segments();
		}

		return $this->segment_labels;
	}

	/**
	 * Compares two report data objects by pre-defined object property and ASC/DESC ordering.
	 *
	 * @param stdClass $a Object a.
	 * @param stdClass $b Object b.
	 * @return string
	 */
	private function segment_cmp( $a, $b ) {
		if ( $a['segment_id'] === $b['segment_id'] ) {
			return 0;
		} elseif ( $a['segment_id'] > $b['segment_id'] ) {
			return 1;
		} elseif ( $a['segment_id'] < $b['segment_id'] ) {
			return - 1;
		}
	}

	/**
	 * Adds zeroes for segments not present in the data selection.
	 *
	 * @param array $segments Array of segments from the database for given data points.
	 *
	 * @return array
	 */
	protected function fill_in_missing_segments( $segments ) {
		$segment_subtotals = array();
		if ( isset( $this->query_args['fields'] ) && is_array( $this->query_args['fields'] ) ) {
			foreach ( $this->query_args['fields'] as $field ) {
				if ( isset( $this->report_columns[ $field ] ) ) {
					$segment_subtotals[ $field ] = 0;
				}
			}
		} else {
			foreach ( $this->report_columns as $field => $sql_clause ) {
				$segment_subtotals[ $field ] = 0;
			}
		}
		if ( ! is_array( $segments ) ) {
			$segments = array();
		}
		$all_segment_ids = $this->get_all_segments();
		$segment_labels  = $this->get_segment_labels();
		foreach ( $all_segment_ids as $segment_id ) {
			if ( ! isset( $segments[ $segment_id ] ) ) {
				$segments[ $segment_id ] = array(
					'segment_id'    => $segment_id,
					'segment_label' => $segment_labels[ $segment_id ],
					'subtotals'     => $segment_subtotals,
				);
			}
		}

		// Using array_values to remove custom keys, so that it gets later converted to JSON as an array.
		$segments_no_keys = array_values( $segments );
		usort( $segments_no_keys, array( $this, 'segment_cmp' ) );
		return $segments_no_keys;
	}

	/**
	 * Adds missing segments to intervals, modifies $data.
	 *
	 * @param stdClass $data Response data.
	 */
	protected function fill_in_missing_interval_segments( &$data ) {
		foreach ( $data->intervals as $order_id => $interval_data ) {
			$data->intervals[ $order_id ]['segments'] = $this->fill_in_missing_segments( $data->intervals[ $order_id ]['segments'] );
		}
	}

	/**
	 * Calculate segments for segmenting property bound to product (e.g. category, product_id, variation_id).
	 *
	 * @param string $type Type of segments to return--'totals' or 'intervals'.
	 * @param array  $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $segmenting_dimension_name Name of the segmenting dimension.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $query_params Array of SQL clauses for intervals/totals query.
	 * @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
	 *
	 * @return array
	 */
	protected function get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ) {
		if ( 'totals' === $type ) {
			return $this->get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
		} elseif ( 'intervals' === $type ) {
			return $this->get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
		}
	}

	/**
	 * Calculate segments for segmenting property bound to order (e.g. coupon or customer type).
	 *
	 * @param string $type Type of segments to return--'totals' or 'intervals'.
	 * @param string $segmenting_select SELECT part of segmenting SQL query.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $query_params Array of SQL clauses for intervals/totals query.
	 *
	 * @return array
	 */
	protected function get_order_related_segments( $type, $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params ) {
		if ( 'totals' === $type ) {
			return $this->get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
		} elseif ( 'intervals' === $type ) {
			return $this->get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
		}
	}

	/**
	 * Assign segments to time intervals by updating original $intervals array.
	 *
	 * @param array $intervals Result array from intervals SQL query.
	 * @param array $intervals_segments Result array from interval segments SQL query.
	 */
	protected function assign_segments_to_intervals( &$intervals, $intervals_segments ) {
		$old_keys = array_keys( $intervals );
		foreach ( $intervals as $interval ) {
			$intervals[ $interval['time_interval'] ]             = $interval;
			$intervals[ $interval['time_interval'] ]['segments'] = array();
		}
		foreach ( $old_keys as $key ) {
			unset( $intervals[ $key ] );
		}

		foreach ( $intervals_segments as $time_interval => $segment ) {
			if ( isset( $intervals[ $time_interval ] ) ) {
				$intervals[ $time_interval ]['segments'] = $segment['segments'];
			}
		}
		// To remove time interval keys (so that REST response is formatted correctly).
		$intervals = array_values( $intervals );
	}

	/**
	 * Returns an array of segments for totals part of REST response.
	 *
	 * @param array  $query_params Totals SQL query parameters.
	 * @param string $table_name Name of the SQL table that is the main order stats table.
	 *
	 * @return array
	 */
	public function get_totals_segments( $query_params, $table_name ) {
		$segments = $this->get_segments( 'totals', $query_params, $table_name );
		$segments = $this->fill_in_missing_segments( $segments );

		return $segments;
	}

	/**
	 * Adds an array of segments to data->intervals object.
	 *
	 * @param stdClass $data Data object representing the REST response.
	 * @param array    $intervals_query Intervals SQL query parameters.
	 * @param string   $table_name Name of the SQL table that is the main order stats table.
	 */
	public function add_intervals_segments( &$data, $intervals_query, $table_name ) {
		$intervals_segments = $this->get_segments( 'intervals', $intervals_query, $table_name );

		$this->assign_segments_to_intervals( $data->intervals, $intervals_segments );
		$this->fill_in_missing_interval_segments( $data );
	}
}
Admin/API/Reports/Downloads/Query.php000064400000002261152333011040013404 0ustar00<?php
/**
 * Class for parameter-based downloads report querying.
 *
 * Example usage:
 * $args = array(
 *      'before'       => '2018-07-19 00:00:00',
 *      'after'        => '2018-07-05 00:00:00',
 *      'page'         => 2,
 *      'products'     => array(1,2,3)
 * );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Downloads\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Downloads;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Downloads\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for downloads report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array();
	}

	/**
	 * Get downloads data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_downloads_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-downloads' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_downloads_select_query', $results, $args );
	}
}
Admin/API/Reports/Downloads/Controller.php000064400000035250152333011040014426 0ustar00<?php
/**
 * REST API Reports downloads controller
 *
 * Handles requests to the /reports/downloads endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Downloads;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;

/**
 * REST API Reports downloads controller class.
 *
 * @internal
 * @extends Automattic\WooCommerce\Admin\API\Reports\Controller
 */
class Controller extends ReportsController implements ExportableInterface {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/downloads';

	/**
	 * Get items.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$args       = array();
		$registered = array_keys( $this->get_collection_params() );
		foreach ( $registered as $param_name ) {
			if ( isset( $request[ $param_name ] ) ) {
				$args[ $param_name ] = $request[ $param_name ];
			}
		}

		$reports        = new Query( $args );
		$downloads_data = $reports->get_data();

		$data = array();

		foreach ( $downloads_data->data as $download_data ) {
			$item   = $this->prepare_item_for_response( $download_data, $request );
			$data[] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $data );

		$response->header( 'X-WP-Total', (int) $downloads_data->total );
		$response->header( 'X-WP-TotalPages', (int) $downloads_data->pages );

		$page      = $downloads_data->page_no;
		$max_pages = $downloads_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param Array           $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $report ) );

		$response->data['date'] = get_date_from_gmt( $data['date_gmt'], 'Y-m-d H:i:s' );

		// Figure out file name.
		// Matches https://github.com/woocommerce/woocommerce/blob/4be0018c092e617c5d2b8c46b800eb71ece9ddef/includes/class-wc-download-handler.php#L197.
		$product_id = intval( $data['product_id'] );
		$_product   = wc_get_product( $product_id );

		// Make sure the product hasn't been deleted.
		if ( $_product ) {
			$file_path                   = $_product->get_file_download_path( $data['download_id'] );
			$filename                    = basename( $file_path );
			$response->data['file_name'] = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id );
			$response->data['file_path'] = $file_path;
		} else {
			$response->data['file_name'] = '';
			$response->data['file_path'] = '';
		}

		$customer                       = new \WC_Customer( $data['user_id'] );
		$response->data['username']     = $customer->get_username();
		$response->data['order_number'] = $this->get_order_number( $data['order_id'] );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_downloads', $response, $report, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param Array $object Object data.
	 * @return array        Links for the given post.
	 */
	protected function prepare_links( $object ) {
		$links = array(
			'product' => array(
				'href'       => rest_url( sprintf( '/%s/%s/%d', $this->namespace, 'products', $object['product_id'] ) ),
				'embeddable' => true,
			),
		);

		return $links;
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_downloads',
			'type'       => 'object',
			'properties' => array(
				'id'           => array(
					'type'        => 'integer',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'ID.', 'woocommerce' ),
				),
				'product_id'   => array(
					'type'        => 'integer',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Product ID.', 'woocommerce' ),
				),
				'date'         => array(
					'description' => __( "The date of the download, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_gmt'     => array(
					'description' => __( 'The date of the download, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'download_id'  => array(
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Download ID.', 'woocommerce' ),
				),
				'file_name'    => array(
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'File name.', 'woocommerce' ),
				),
				'file_path'    => array(
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'File URL.', 'woocommerce' ),
				),
				'order_id'     => array(
					'type'        => 'integer',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Order ID.', 'woocommerce' ),
				),
				'order_number' => array(
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Order Number.', 'woocommerce' ),
				),
				'user_id'      => array(
					'type'        => 'integer',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'User ID for the downloader.', 'woocommerce' ),
				),
				'username'     => array(
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'User name of the downloader.', 'woocommerce' ),
				),
				'ip_address'   => array(
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'IP address for the downloader.', 'woocommerce' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                        = array();
		$params['context']             = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']                = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']            = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']               = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']              = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']               = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']             = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'date',
				'product',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['match']               = array(
			'description'       => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: products, orders, username, ip_address.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'all',
			'enum'              => array(
				'all',
				'any',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['product_includes']    = array(
			'description'       => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['product_excludes']    = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['order_includes']      = array(
			'description'       => __( 'Limit result set to items that have the specified order ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['order_excludes']      = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified order ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['customer_includes']   = array(
			'description'       => __( 'Limit response to objects that have the specified user ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['customer_excludes']   = array(
			'description'       => __( 'Limit response to objects that don\'t have the specified user ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['ip_address_includes'] = array(
			'description'       => __( 'Limit response to objects that have a specified ip address.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['ip_address_excludes'] = array(
			'description'       => __( 'Limit response to objects that don\'t have a specified ip address.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get the column names for export.
	 *
	 * @return array Key value pair of Column ID => Label.
	 */
	public function get_export_columns() {
		$export_columns = array(
			'date'         => __( 'Date', 'woocommerce' ),
			'product'      => __( 'Product title', 'woocommerce' ),
			'file_name'    => __( 'File name', 'woocommerce' ),
			'order_number' => __( 'Order #', 'woocommerce' ),
			'user_id'      => __( 'User Name', 'woocommerce' ),
			'ip_address'   => __( 'IP', 'woocommerce' ),
		);

		/**
		 * Filter to add or remove column names from the downloads report for
		 * export.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_filter_downloads_export_columns',
			$export_columns
		);
	}

	/**
	 * Get the column values for export.
	 *
	 * @param array $item Single report item/row.
	 * @return array Key value pair of Column ID => Row Value.
	 */
	public function prepare_item_for_export( $item ) {
		$export_item = array(
			'date'         => $item['date'],
			'product'      => $item['_embedded']['product'][0]['name'],
			'file_name'    => $item['file_name'],
			'order_number' => $item['order_number'],
			'user_id'      => $item['username'],
			'ip_address'   => $item['ip_address'],
		);

		/**
		 * Filter to prepare extra columns in the export item for the downloads
		 * report.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_report_downloads_prepare_export_item',
			$export_item,
			$item
		);
	}
}
Admin/API/Reports/Downloads/DataStore.php000064400000030627152333011040014174 0ustar00<?php
/**
 * API\Reports\Downloads\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Downloads;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;

/**
 * API\Reports\Downloads\DataStore.
 */
class DataStore extends ReportsDataStore implements DataStoreInterface {

	/**
	 * Table used to get the data.
	 *
	 * @var string
	 */
	protected static $table_name = 'wc_download_log';

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'downloads';

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'id'          => 'intval',
		'date'        => 'strval',
		'date_gmt'    => 'strval',
		'download_id' => 'strval', // String because this can sometimes be a hash.
		'file_name'   => 'strval',
		'product_id'  => 'intval',
		'order_id'    => 'intval',
		'user_id'     => 'intval',
		'ip_address'  => 'strval',
	);

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'downloads';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$this->report_columns = array(
			'id'          => 'download_log_id as id',
			'date'        => 'timestamp as date_gmt',
			'download_id' => 'product_permissions.download_id',
			'product_id'  => 'product_permissions.product_id',
			'order_id'    => 'product_permissions.order_id',
			'user_id'     => 'product_permissions.user_id',
			'ip_address'  => 'user_ip_address as ip_address',
		);
	}

	/**
	 * Updates the database query with parameters used for downloads report.
	 *
	 * @param array $query_args Query arguments supplied by the user.
	 */
	protected function add_sql_query_params( $query_args ) {
		global $wpdb;

		$lookup_table     = self::get_db_table_name();
		$permission_table = $wpdb->prefix . 'woocommerce_downloadable_product_permissions';
		$operator         = $this->get_match_operator( $query_args );
		$where_filters    = array();
		$join             = "JOIN {$permission_table} as product_permissions ON {$lookup_table}.permission_id = product_permissions.permission_id";

		$where_time = $this->add_time_period_sql_params( $query_args, $lookup_table );
		if ( $where_time ) {
			if ( isset( $this->subquery ) ) {
				$this->subquery->add_sql_clause( 'where_time', $where_time );
			} else {
				$this->interval_query->add_sql_clause( 'where_time', $where_time );
			}
		}
		$this->get_limit_sql_params( $query_args );

		$where_filters[] = $this->get_object_where_filter(
			$lookup_table,
			'permission_id',
			$permission_table,
			'product_id',
			'IN',
			$this->get_included_products( $query_args )
		);
		$where_filters[] = $this->get_object_where_filter(
			$lookup_table,
			'permission_id',
			$permission_table,
			'product_id',
			'NOT IN',
			$this->get_excluded_products( $query_args )
		);
		$where_filters[] = $this->get_object_where_filter(
			$lookup_table,
			'permission_id',
			$permission_table,
			'order_id',
			'IN',
			$this->get_included_orders( $query_args )
		);
		$where_filters[] = $this->get_object_where_filter(
			$lookup_table,
			'permission_id',
			$permission_table,
			'order_id',
			'NOT IN',
			$this->get_excluded_orders( $query_args )
		);

		$customer_lookup_table = $wpdb->prefix . 'wc_customer_lookup';
		$customer_lookup       = "SELECT {$customer_lookup_table}.user_id FROM {$customer_lookup_table} WHERE {$customer_lookup_table}.customer_id IN (%s)";
		$included_customers    = $this->get_included_customers( $query_args );
		$excluded_customers    = $this->get_excluded_customers( $query_args );
		if ( $included_customers ) {
			$where_filters[] = $this->get_object_where_filter(
				$lookup_table,
				'permission_id',
				$permission_table,
				'user_id',
				'IN',
				sprintf( $customer_lookup, $included_customers )
			);
		}

		if ( $excluded_customers ) {
			$where_filters[] = $this->get_object_where_filter(
				$lookup_table,
				'permission_id',
				$permission_table,
				'user_id',
				'NOT IN',
				sprintf( $customer_lookup, $excluded_customers )
			);
		}

		$included_ip_addresses = $this->get_included_ip_addresses( $query_args );
		$excluded_ip_addresses = $this->get_excluded_ip_addresses( $query_args );
		if ( $included_ip_addresses ) {
			$where_filters[] = "{$lookup_table}.user_ip_address IN ('{$included_ip_addresses}')";
		}

		if ( $excluded_ip_addresses ) {
			$where_filters[] = "{$lookup_table}.user_ip_address NOT IN ('{$excluded_ip_addresses}')";
		}

		$where_filters   = array_filter( $where_filters );
		$where_subclause = implode( " $operator ", $where_filters );
		if ( $where_subclause ) {
			if ( isset( $this->subquery ) ) {
				$this->subquery->add_sql_clause( 'where', "AND ( $where_subclause )" );
			} else {
				$this->interval_query->add_sql_clause( 'where', "AND ( $where_subclause )" );
			}
		}

		if ( isset( $this->subquery ) ) {
			$this->subquery->add_sql_clause( 'join', $join );
		} else {
			$this->interval_query->add_sql_clause( 'join', $join );
		}
		$this->add_order_by( $query_args );
	}

	/**
	 * Returns comma separated ids of included ip address, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_included_ip_addresses( $query_args ) {
		return $this->get_filtered_ip_addresses( $query_args, 'ip_address_includes' );
	}

	/**
	 * Returns comma separated ids of excluded ip address, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_excluded_ip_addresses( $query_args ) {
		return $this->get_filtered_ip_addresses( $query_args, 'ip_address_excludes' );
	}

	/**
	 * Returns filtered comma separated ids, based on query arguments from the user.
	 *
	 * @param array  $query_args  Parameters supplied by the user.
	 * @param string $field       Query field to filter.
	 * @return string
	 */
	protected function get_filtered_ip_addresses( $query_args, $field ) {
		if ( isset( $query_args[ $field ] ) && is_array( $query_args[ $field ] ) && count( $query_args[ $field ] ) > 0 ) {
			$ip_addresses = array_map( 'esc_sql', $query_args[ $field ] );

			/**
			 * Filter the IDs before retrieving report data.
			 *
			 * Allows filtering of the objects included or excluded from reports.
			 *
			 * @param array  $ids        List of object Ids.
			 * @param array  $query_args The original arguments for the request.
			 * @param string $field      The object type.
			 * @param string $context    The data store context.
			 */
			$ip_addresses = apply_filters( 'woocommerce_analytics_' . $field, $ip_addresses, $query_args, $field, $this->context );

			return implode( "','", $ip_addresses );
		}
		return '';
	}

	/**
	 * Returns comma separated ids of included customers, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_included_customers( $query_args ) {
		return self::get_filtered_ids( $query_args, 'customer_includes' );
	}

	/**
	 * Returns comma separated ids of excluded customers, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return string
	 */
	protected function get_excluded_customers( $query_args ) {
		return self::get_filtered_ids( $query_args, 'customer_excludes' );
	}

	/**
	 * Gets WHERE time clause of SQL request with date-related constraints.
	 *
	 * @param array  $query_args Parameters supplied by the user.
	 * @param string $table_name Name of the db table relevant for the date constraint.
	 * @return string
	 */
	protected function add_time_period_sql_params( $query_args, $table_name ) {
		$where_time = '';
		if ( $query_args['before'] ) {
			$datetime_str = $query_args['before']->format( TimeInterval::$sql_datetime_format );
			$where_time  .= " AND {$table_name}.timestamp <= '$datetime_str'";

		}

		if ( $query_args['after'] ) {
			$datetime_str = $query_args['after']->format( TimeInterval::$sql_datetime_format );
			$where_time  .= " AND {$table_name}.timestamp >= '$datetime_str'";
		}

		return $where_time;
	}

	/**
	 * Fills ORDER BY clause of SQL request based on user supplied parameters.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 */
	protected function add_order_by( $query_args ) {
		global $wpdb;
		$this->clear_sql_clause( 'order_by' );
		$order_by = '';
		if ( isset( $query_args['orderby'] ) ) {
			$order_by = $this->normalize_order_by( esc_sql( $query_args['orderby'] ) );
			$this->add_sql_clause( 'order_by', $order_by );
		}

		if ( false !== strpos( $order_by, '_products' ) ) {
			$this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->posts} AS _products ON product_permissions.product_id = _products.ID" );
		}

		$this->add_orderby_order_clause( $query_args, $this );
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page' => get_option( 'posts_per_page' ),
			'page'     => 1,
			'order'    => 'DESC',
			'orderby'  => 'timestamp',
			'before'   => TimeInterval::default_before(),
			'after'    => TimeInterval::default_after(),
			'fields'   => '*',
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$data = (object) array(
				'data'    => array(),
				'total'   => 0,
				'pages'   => 0,
				'page_no' => 0,
			);

			$selections = $this->selected_columns( $query_args );
			$this->add_sql_query_params( $query_args );

			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$db_records_count = (int) $wpdb->get_var(
				"SELECT COUNT(*) FROM (
					{$this->subquery->get_query_statement()}
				) AS tt"
			);
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

			$params      = $this->get_limit_params( $query_args );
			$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
			if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
				return $data;
			}

			$this->subquery->clear_sql_clause( 'select' );
			$this->subquery->add_sql_clause( 'select', $selections );
			$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
			$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );

			$download_data = $wpdb->get_results(
				$this->subquery->get_query_statement(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				ARRAY_A
			);

			if ( null === $download_data ) {
				return $data;
			}

			$download_data = array_map( array( $this, 'cast_numbers' ), $download_data );
			$data          = (object) array(
				'data'    => $download_data,
				'total'   => $db_records_count,
				'pages'   => $total_pages,
				'page_no' => (int) $query_args['page'],
			);

			$this->set_cached_data( $cache_key, $data );
		}

		return $data;
	}

	/**
	 * Maps ordering specified by the user to columns in the database/fields in the data.
	 *
	 * @param string $order_by Sorting criterion.
	 * @return string
	 */
	protected function normalize_order_by( $order_by ) {
		global $wpdb;

		if ( 'date' === $order_by ) {
			return $wpdb->prefix . 'wc_download_log.timestamp';
		}

		if ( 'product' === $order_by ) {
			return '_products.post_title';
		}

		return $order_by;
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		$table_name     = self::get_db_table_name();
		$this->subquery = new SqlQuery( $this->context . '_subquery' );
		$this->subquery->add_sql_clause( 'from', $table_name );
		$this->subquery->add_sql_clause( 'select', "{$table_name}.download_log_id" );
		$this->subquery->add_sql_clause( 'group_by', "{$table_name}.download_log_id" );
	}
}
Admin/API/Reports/Downloads/Stats/Controller.php000064400000033001152333011040015514 0ustar00<?php
/**
 * REST API Reports downloads stats controller
 *
 * Handles requests to the /reports/downloads/stats endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats;

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports downloads stats controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/downloads/stats';

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args                        = array();
		$args['before']              = $request['before'];
		$args['after']               = $request['after'];
		$args['interval']            = $request['interval'];
		$args['page']                = $request['page'];
		$args['per_page']            = $request['per_page'];
		$args['orderby']             = $request['orderby'];
		$args['order']               = $request['order'];
		$args['match']               = $request['match'];
		$args['product_includes']    = (array) $request['product_includes'];
		$args['product_excludes']    = (array) $request['product_excludes'];
		$args['customer_includes']   = (array) $request['customer_includes'];
		$args['customer_excludes']   = (array) $request['customer_excludes'];
		$args['order_includes']      = (array) $request['order_includes'];
		$args['order_excludes']      = (array) $request['order_excludes'];
		$args['ip_address_includes'] = (array) $request['ip_address_includes'];
		$args['ip_address_excludes'] = (array) $request['ip_address_excludes'];
		$args['fields']              = $request['fields'];
		$args['force_cache_refresh'] = $request['force_cache_refresh'];

		return $args;
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$query_args      = $this->prepare_reports_query( $request );
		$downloads_query = new Query( $query_args );
		$report_data     = $downloads_query->get_data();

		$out_data = array(
			'totals'    => get_object_vars( $report_data->totals ),
			'intervals' => array(),
		);

		foreach ( $report_data->intervals as $interval_data ) {
			$item                    = $this->prepare_item_for_response( $interval_data, $request );
			$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $out_data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		$page      = $report_data->page_no;
		$max_pages = $report_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param Array           $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_downloads_stats', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$totals = array(
			'download_count' => array(
				'title'       => __( 'Downloads', 'woocommerce' ),
				'description' => __( 'Number of downloads.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
			),
		);

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_orders_stats',
			'type'       => 'object',
			'properties' => array(
				'totals'    => array(
					'description' => __( 'Totals data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'properties'  => $totals,
				),
				'intervals' => array(
					'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'interval'       => array(
								'description' => __( 'Type of interval.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'enum'        => array( 'day', 'week', 'month', 'year' ),
							),
							'date_start'     => array(
								'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_start_gmt' => array(
								'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end'       => array(
								'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end_gmt'   => array(
								'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'subtotals'      => array(
								'description' => __( 'Interval subtotals.', 'woocommerce' ),
								'type'        => 'object',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'properties'  => $totals,
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                     = array();
		$params['context']          = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']             = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']         = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']            = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']           = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']            = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']          = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'date',
				'download_count',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['interval']         = array(
			'description'       => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'week',
			'enum'              => array(
				'hour',
				'day',
				'week',
				'month',
				'quarter',
				'year',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['match']            = array(
			'description'       => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'all',
			'enum'              => array(
				'all',
				'any',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['product_includes'] = array(
			'description'       => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',

		);
		$params['product_excludes']    = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['order_includes']      = array(
			'description'       => __( 'Limit result set to items that have the specified order ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['order_excludes']      = array(
			'description'       => __( 'Limit result set to items that don\'t have the specified order ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['customer_includes']   = array(
			'description'       => __( 'Limit response to objects that have the specified customer ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['customer_excludes']   = array(
			'description'       => __( 'Limit response to objects that don\'t have the specified customer ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['ip_address_includes'] = array(
			'description'       => __( 'Limit response to objects that have a specified ip address.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);

		$params['ip_address_excludes'] = array(
			'description'       => __( 'Limit response to objects that don\'t have a specified ip address.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['fields']              = array(
			'description'       => __( 'Limit stats fields to the specified items.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
Admin/API/Reports/Downloads/Stats/DataStore.php000064400000015323152333011040015266 0ustar00<?php
/**
 * API\Reports\Downloads\Stats\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Downloads\DataStore as DownloadsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;

/**
 * API\Reports\Downloads\Stats\DataStore.
 */
class DataStore extends DownloadsDataStore implements DataStoreInterface {

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'download_count' => 'intval',
	);

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'downloads_stats';

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'downloads_stats';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$this->report_columns = array(
			'download_count' => 'COUNT(DISTINCT download_log_id) as download_count',
		);
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page' => get_option( 'posts_per_page' ),
			'page'     => 1,
			'order'    => 'DESC',
			'orderby'  => 'date',
			'fields'   => '*',
			'interval' => 'week',
			'before'   => TimeInterval::default_before(),
			'after'    => TimeInterval::default_after(),
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();
			$selections = $this->selected_columns( $query_args );
			$this->add_sql_query_params( $query_args );
			$where_time = $this->add_time_period_sql_params( $query_args, $table_name );
			$this->add_intervals_sql_params( $query_args, $table_name );

			$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
			$this->interval_query->str_replace_clause( 'select', 'date_created', 'timestamp' );
			$this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' );

			$db_intervals = $wpdb->get_col(
				$this->interval_query->get_query_statement()
			); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.

			$db_records_count = count( $db_intervals );

			$params                  = $this->get_limit_params( $query_args );
			$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
			$total_pages             = (int) ceil( $expected_interval_count / $params['per_page'] );
			if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
				return array();
			}

			$this->update_intervals_sql_params( $query_args, $db_records_count, $expected_interval_count, $table_name );
			$this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' );
			$this->total_query->add_sql_clause( 'select', $selections );
			$this->total_query->add_sql_clause( 'where', $this->interval_query->get_sql_clause( 'where' ) );
			if ( $where_time ) {
				$this->total_query->add_sql_clause( 'where_time', $where_time );
			}
			$totals = $wpdb->get_results(
				$this->total_query->get_query_statement(),
				ARRAY_A
			); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
			if ( null === $totals ) {
				return new \WP_Error( 'woocommerce_analytics_downloads_stats_result_failed', __( 'Sorry, fetching downloads data failed.', 'woocommerce' ) );
			}

			$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
			$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
			$this->interval_query->add_sql_clause( 'select', ', MAX(timestamp) AS datetime_anchor' );
			if ( '' !== $selections ) {
				$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
			}

			$intervals = $wpdb->get_results(
				$this->interval_query->get_query_statement(),
				ARRAY_A
			); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.

			if ( null === $intervals ) {
				return new \WP_Error( 'woocommerce_analytics_downloads_stats_result_failed', __( 'Sorry, fetching downloads data failed.', 'woocommerce' ) );
			}

			$totals = (object) $this->cast_numbers( $totals[0] );
			$data   = (object) array(
				'totals'    => $totals,
				'intervals' => $intervals,
				'total'     => $expected_interval_count,
				'pages'     => $total_pages,
				'page_no'   => (int) $query_args['page'],
			);

			if ( $this->intervals_missing( $expected_interval_count, $db_records_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
				$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
				$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
				$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_records_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
			} else {
				$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
			}
			$this->create_interval_subtotals( $data->intervals );

			$this->set_cached_data( $cache_key, $data );
		}

		return $data;
	}

	/**
	 * Normalizes order_by clause to match to SQL query.
	 *
	 * @param string $order_by Order by option requeste by user.
	 * @return string
	 */
	protected function normalize_order_by( $order_by ) {
		if ( 'date' === $order_by ) {
			return 'time_interval';
		}

		return $order_by;
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		unset( $this->subquery );
		$this->total_query = new SqlQuery( $this->context . '_total' );
		$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );

		$this->interval_query = new SqlQuery( $this->context . '_interval' );
		$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
		$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
	}
}
Admin/API/Reports/Downloads/Stats/Query.php000064400000001600152333011040014476 0ustar00<?php
/**
 * Class for parameter-based downloads Reports querying
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Downloads\Stats\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Orders report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array();
	}

	/**
	 * Get revenue data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_downloads_stats_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-downloads-stats' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_downloads_stats_select_query', $results, $args );
	}
}
Admin/API/Reports/Downloads/Files/Controller.php000064400000001122152333011040015457 0ustar00<?php
/**
 * REST API Reports downloads files controller
 *
 * Handles requests to the /reports/downloads/files endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Downloads\Files;

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports downloads files controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/downloads/files';
}
Admin/API/Reports/Coupons/DataStore.php000064400000036741152333011040013673 0ustar00<?php
/**
 * API\Reports\Coupons\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Coupons;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;

/**
 * API\Reports\Coupons\DataStore.
 */
class DataStore extends ReportsDataStore implements DataStoreInterface {

	/**
	 * Table used to get the data.
	 *
	 * @var string
	 */
	protected static $table_name = 'wc_order_coupon_lookup';

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'coupons';

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'coupon_id'    => 'intval',
		'amount'       => 'floatval',
		'orders_count' => 'intval',
	);

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'coupons';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$table_name           = self::get_db_table_name();
		$this->report_columns = array(
			'coupon_id'    => 'coupon_id',
			'amount'       => 'SUM(discount_amount) as amount',
			'orders_count' => "COUNT(DISTINCT {$table_name}.order_id) as orders_count",
		);
	}

	/**
	 * Set up all the hooks for maintaining and populating table data.
	 */
	public static function init() {
		add_action( 'woocommerce_analytics_delete_order_stats', array( __CLASS__, 'sync_on_order_delete' ), 5 );
	}

	/**
	 * Returns an array of ids of included coupons, based on query arguments from the user.
	 *
	 * @param array $query_args Parameters supplied by the user.
	 * @return array
	 */
	protected function get_included_coupons_array( $query_args ) {
		if ( isset( $query_args['coupons'] ) && is_array( $query_args['coupons'] ) && count( $query_args['coupons'] ) > 0 ) {
			return $query_args['coupons'];
		}
		return array();
	}

	/**
	 * Updates the database query with parameters used for Products report: categories and order status.
	 *
	 * @param array $query_args Query arguments supplied by the user.
	 */
	protected function add_sql_query_params( $query_args ) {
		global $wpdb;
		$order_coupon_lookup_table = self::get_db_table_name();

		$this->add_time_period_sql_params( $query_args, $order_coupon_lookup_table );
		$this->get_limit_sql_params( $query_args );

		$included_coupons = $this->get_included_coupons( $query_args, 'coupons' );
		if ( $included_coupons ) {
			$this->subquery->add_sql_clause( 'where', "AND {$order_coupon_lookup_table}.coupon_id IN ({$included_coupons})" );

			$this->add_order_by_params( $query_args, 'outer', 'default_results.coupon_id' );
		} else {
			$this->add_order_by_params( $query_args, 'inner', "{$order_coupon_lookup_table}.coupon_id" );
		}

		$this->add_order_status_clause( $query_args, $order_coupon_lookup_table, $this->subquery );
	}

	/**
	 * Fills ORDER BY clause of SQL request based on user supplied parameters.
	 *
	 * @param array  $query_args Parameters supplied by the user.
	 * @param string $from_arg   Target of the JOIN sql param.
	 * @param string $id_cell    ID cell identifier, like `table_name.id_column_name`.
	 */
	protected function add_order_by_params( $query_args, $from_arg, $id_cell ) {
		global $wpdb;

		// Sanitize input: guarantee that the id cell in the join is quoted with backticks.
		$id_cell_segments   = explode( '.', str_replace( '`', '', $id_cell ) );
		$id_cell_identifier = '`' . implode( '`.`', $id_cell_segments ) . '`';

		$lookup_table    = self::get_db_table_name();
		$order_by_clause = $this->add_order_by_clause( $query_args, $this );
		$join            = "JOIN {$wpdb->posts} AS _coupons ON {$id_cell_identifier} = _coupons.ID";
		$this->add_orderby_order_clause( $query_args, $this );

		if ( 'inner' === $from_arg ) {
			$this->subquery->clear_sql_clause( 'join' );
			if ( false !== strpos( $order_by_clause, '_coupons' ) ) {
				$this->subquery->add_sql_clause( 'join', $join );
			}
		} else {
			$this->clear_sql_clause( 'join' );
			if ( false !== strpos( $order_by_clause, '_coupons' ) ) {
				$this->add_sql_clause( 'join', $join );
			}
		}
	}

	/**
	 * Maps ordering specified by the user to columns in the database/fields in the data.
	 *
	 * @param string $order_by Sorting criterion.
	 * @return string
	 */
	protected function normalize_order_by( $order_by ) {
		if ( 'date' === $order_by ) {
			return 'time_interval';
		}
		if ( 'code' === $order_by ) {
			return '_coupons.post_title';
		}
		return $order_by;
	}

	/**
	 * Enriches the coupon data with extra attributes.
	 *
	 * @param array $coupon_data Coupon data.
	 * @param array $query_args Query parameters.
	 */
	protected function include_extended_info( &$coupon_data, $query_args ) {
		foreach ( $coupon_data as $idx => $coupon_datum ) {
			$extended_info = new \ArrayObject();
			if ( $query_args['extended_info'] ) {
				$coupon_id = $coupon_datum['coupon_id'];
				$coupon    = new \WC_Coupon( $coupon_id );

				if ( 0 === $coupon->get_id() ) {
					// Deleted or otherwise invalid coupon.
					$extended_info = array(
						'code'             => __( '(Deleted)', 'woocommerce' ),
						'date_created'     => '',
						'date_created_gmt' => '',
						'date_expires'     => '',
						'date_expires_gmt' => '',
						'discount_type'    => __( 'N/A', 'woocommerce' ),
					);
				} else {
					$gmt_timzone = new \DateTimeZone( 'UTC' );

					$date_expires = $coupon->get_date_expires();
					if ( is_a( $date_expires, 'DateTime' ) ) {
						$date_expires     = $date_expires->format( TimeInterval::$iso_datetime_format );
						$date_expires_gmt = new \DateTime( $date_expires );
						$date_expires_gmt->setTimezone( $gmt_timzone );
						$date_expires_gmt = $date_expires_gmt->format( TimeInterval::$iso_datetime_format );
					} else {
						$date_expires     = '';
						$date_expires_gmt = '';
					}

					$date_created = $coupon->get_date_created();
					if ( is_a( $date_created, 'DateTime' ) ) {
						$date_created     = $date_created->format( TimeInterval::$iso_datetime_format );
						$date_created_gmt = new \DateTime( $date_created );
						$date_created_gmt->setTimezone( $gmt_timzone );
						$date_created_gmt = $date_created_gmt->format( TimeInterval::$iso_datetime_format );
					} else {
						$date_created     = '';
						$date_created_gmt = '';
					}

					$extended_info = array(
						'code'             => $coupon->get_code(),
						'date_created'     => $date_created,
						'date_created_gmt' => $date_created_gmt,
						'date_expires'     => $date_expires,
						'date_expires_gmt' => $date_expires_gmt,
						'discount_type'    => $coupon->get_discount_type(),
					);
				}
			}
			$coupon_data[ $idx ]['extended_info'] = $extended_info;
		}
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page'      => get_option( 'posts_per_page' ),
			'page'          => 1,
			'order'         => 'DESC',
			'orderby'       => 'coupon_id',
			'before'        => TimeInterval::default_before(),
			'after'         => TimeInterval::default_after(),
			'fields'        => '*',
			'coupons'       => array(),
			'extended_info' => false,
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$data = (object) array(
				'data'    => array(),
				'total'   => 0,
				'pages'   => 0,
				'page_no' => 0,
			);

			$selections       = $this->selected_columns( $query_args );
			$included_coupons = $this->get_included_coupons_array( $query_args );
			$limit_params     = $this->get_limit_params( $query_args );
			$this->subquery->add_sql_clause( 'select', $selections );
			$this->add_sql_query_params( $query_args );

			if ( count( $included_coupons ) > 0 ) {
				$total_results = count( $included_coupons );
				$total_pages   = (int) ceil( $total_results / $limit_params['per_page'] );

				$fields    = $this->get_fields( $query_args );
				$ids_table = $this->get_ids_table( $included_coupons, 'coupon_id' );

				$this->add_sql_clause( 'select', $this->format_join_selections( $fields, array( 'coupon_id' ) ) );
				$this->add_sql_clause( 'from', '(' );
				$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
				$this->add_sql_clause( 'from', ") AS {$table_name}" );
				$this->add_sql_clause(
					'right_join',
					"RIGHT JOIN ( {$ids_table} ) AS default_results
					ON default_results.coupon_id = {$table_name}.coupon_id"
				);

				$coupons_query = $this->get_query_statement();
			} else {
				$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
				$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
				$coupons_query = $this->subquery->get_query_statement();

				$this->subquery->clear_sql_clause( array( 'select', 'order_by', 'limit' ) );
				$this->subquery->add_sql_clause( 'select', 'coupon_id' );
				$coupon_subquery = "SELECT COUNT(*) FROM (
					{$this->subquery->get_query_statement()}
				) AS tt";

				$db_records_count = (int) $wpdb->get_var(
					$coupon_subquery // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				);

				$total_results = $db_records_count;
				$total_pages   = (int) ceil( $db_records_count / $limit_params['per_page'] );
				if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
					return $data;
				}
			}

			$coupon_data = $wpdb->get_results(
				$coupons_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				ARRAY_A
			);
			if ( null === $coupon_data ) {
				return $data;
			}

			$this->include_extended_info( $coupon_data, $query_args );

			$coupon_data = array_map( array( $this, 'cast_numbers' ), $coupon_data );
			$data        = (object) array(
				'data'    => $coupon_data,
				'total'   => $total_results,
				'pages'   => $total_pages,
				'page_no' => (int) $query_args['page'],
			);

			$this->set_cached_data( $cache_key, $data );
		}

		return $data;
	}

	/**
	 * Get coupon ID for an order.
	 *
	 * Tries to get the ID from order item meta, then falls back to a query of published coupons.
	 *
	 * @param \WC_Order_Item_Coupon $coupon_item The coupon order item object.
	 * @return int Coupon ID on success, 0 on failure.
	 */
	public static function get_coupon_id( \WC_Order_Item_Coupon $coupon_item ) {
		// First attempt to get coupon ID from order item data.
		$coupon_data = $coupon_item->get_meta( 'coupon_data', true );

		// Normal checkout orders should have this data.
		// See: https://github.com/woocommerce/woocommerce/blob/3dc7df7af9f7ca0c0aa34ede74493e856f276abe/includes/abstracts/abstract-wc-order.php#L1206.
		if ( isset( $coupon_data['id'] ) ) {
			return $coupon_data['id'];
		}

		// Try to get the coupon ID using the code.
		return wc_get_coupon_id_by_code( $coupon_item->get_code() );
	}

	/**
	 * Create or update an an entry in the wc_order_coupon_lookup table for an order.
	 *
	 * @since 3.5.0
	 * @param int $order_id Order ID.
	 * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
	 */
	public static function sync_order_coupons( $order_id ) {
		global $wpdb;

		$order = wc_get_order( $order_id );

		if ( ! $order ) {
			return -1;
		}

		// Refunds don't affect coupon stats so return successfully if one is called here.
		if ( 'shop_order_refund' === $order->get_type() ) {
			return true;
		}

		$table_name     = self::get_db_table_name();
		$existing_items = $wpdb->get_col(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT coupon_id FROM {$table_name} WHERE order_id = %d",
				$order_id
			)
		);
		$existing_items     = array_flip( $existing_items );
		$coupon_items       = $order->get_items( 'coupon' );
		$coupon_items_count = count( $coupon_items );
		$num_updated        = 0;
		$num_deleted        = 0;

		foreach ( $coupon_items as $coupon_item ) {
			$coupon_id = self::get_coupon_id( $coupon_item );
			unset( $existing_items[ $coupon_id ] );

			if ( ! $coupon_id ) {
				// Insert a unique, but obviously invalid ID for this deleted coupon.
				$num_deleted++;
				$coupon_id = -1 * $num_deleted;
			}

			$result = $wpdb->replace(
				self::get_db_table_name(),
				array(
					'order_id'        => $order_id,
					'coupon_id'       => $coupon_id,
					'discount_amount' => $coupon_item->get_discount(),
					'date_created'    => $order->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ),
				),
				array(
					'%d',
					'%d',
					'%f',
					'%s',
				)
			);

			/**
			 * Fires when coupon's reports are updated.
			 *
			 * @param int $coupon_id Coupon ID.
			 * @param int $order_id  Order ID.
			 */
			do_action( 'woocommerce_analytics_update_coupon', $coupon_id, $order_id );

			// Sum the rows affected. Using REPLACE can affect 2 rows if the row already exists.
			$num_updated += 2 === intval( $result ) ? 1 : intval( $result );
		}

		if ( ! empty( $existing_items ) ) {
			$existing_items = array_flip( $existing_items );
			$format         = array_fill( 0, count( $existing_items ), '%d' );
			$format         = implode( ',', $format );
			array_unshift( $existing_items, $order_id );
			$wpdb->query(
				$wpdb->prepare(
					// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					"DELETE FROM {$table_name} WHERE order_id = %d AND coupon_id in ({$format})",
					$existing_items
				)
			);
		}

		return ( $coupon_items_count === $num_updated );
	}

	/**
	 * Clean coupons data when an order is deleted.
	 *
	 * @param int $order_id Order ID.
	 */
	public static function sync_on_order_delete( $order_id ) {
		global $wpdb;

		$wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) );
		/**
		 * Fires when coupon's reports are removed from database.
		 *
		 * @param int $coupon_id Coupon ID.
		 * @param int $order_id  Order ID.
		 */
		do_action( 'woocommerce_analytics_delete_coupon', 0, $order_id );

		ReportsCache::invalidate();
	}

	/**
	 * Gets coupons based on the provided arguments.
	 *
	 * @todo Upon core merge, including this in core's `class-wc-coupon-data-store-cpt.php` might make more sense.
	 * @param array $args Array of args to filter the query by. Supports `include`.
	 * @return array Array of results.
	 */
	public function get_coupons( $args ) {
		global $wpdb;
		$query = "SELECT ID, post_title FROM {$wpdb->posts} WHERE post_type='shop_coupon'";

		$included_coupons = $this->get_included_coupons( $args, 'include' );
		if ( ! empty( $included_coupons ) ) {
			$query .= " AND ID IN ({$included_coupons})";
		}

		return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		$this->subquery = new SqlQuery( $this->context . '_subquery' );
		$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
		$this->subquery->add_sql_clause( 'group_by', 'coupon_id' );
	}
}
Admin/API/Reports/Coupons/Controller.php000064400000026177152333011040014132 0ustar00<?php
/**
 * REST API Reports coupons controller
 *
 * Handles requests to the /reports/coupons endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Coupons;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;

/**
 * REST API Reports coupons controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/coupons';

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args                        = array();
		$args['before']              = $request['before'];
		$args['after']               = $request['after'];
		$args['page']                = $request['page'];
		$args['per_page']            = $request['per_page'];
		$args['orderby']             = $request['orderby'];
		$args['order']               = $request['order'];
		$args['coupons']             = (array) $request['coupons'];
		$args['extended_info']       = $request['extended_info'];
		$args['force_cache_refresh'] = $request['force_cache_refresh'];
		return $args;
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$query_args    = $this->prepare_reports_query( $request );
		$coupons_query = new Query( $query_args );
		$report_data   = $coupons_query->get_data();

		$data = array();

		foreach ( $report_data->data as $coupons_data ) {
			$item   = $this->prepare_item_for_response( $coupons_data, $request );
			$data[] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		$page      = $report_data->page_no;
		$max_pages = $report_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param stdClass        $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $report ) );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_coupons', $response, $report, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WC_Reports_Query $object Object data.
	 * @return array
	 */
	protected function prepare_links( $object ) {
		$links = array(
			'coupon' => array(
				'href' => rest_url( sprintf( '/%s/coupons/%d', $this->namespace, $object['coupon_id'] ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_coupons',
			'type'       => 'object',
			'properties' => array(
				'coupon_id'     => array(
					'description' => __( 'Coupon ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'amount'        => array(
					'description' => __( 'Net discount amount.', 'woocommerce' ),
					'type'        => 'number',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'orders_count'  => array(
					'description' => __( 'Number of orders.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'extended_info' => array(
					'code'             => array(
						'type'        => 'string',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Coupon code.', 'woocommerce' ),
					),
					'date_created'     => array(
						'type'        => 'date-time',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Coupon creation date.', 'woocommerce' ),
					),
					'date_created_gmt' => array(
						'type'        => 'date-time',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Coupon creation date in GMT.', 'woocommerce' ),
					),
					'date_expires'     => array(
						'type'        => 'date-time',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Coupon expiration date.', 'woocommerce' ),
					),
					'date_expires_gmt' => array(
						'type'        => 'date-time',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Coupon expiration date in GMT.', 'woocommerce' ),
					),
					'discount_type'    => array(
						'type'        => 'string',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'enum'        => array_keys( wc_get_coupon_types() ),
						'description' => __( 'Coupon discount type.', 'woocommerce' ),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                  = array();
		$params['context']       = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']          = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']      = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']         = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']        = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']         = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']       = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'coupon_id',
			'enum'              => array(
				'coupon_id',
				'code',
				'amount',
				'orders_count',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['coupons']       = array(
			'description'       => __( 'Limit result set to coupons assigned specific coupon IDs.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['extended_info'] = array(
			'description'       => __( 'Add additional piece of info about each coupon to the report.', 'woocommerce' ),
			'type'              => 'boolean',
			'default'           => false,
			'sanitize_callback' => 'wc_string_to_bool',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get the column names for export.
	 *
	 * @return array Key value pair of Column ID => Label.
	 */
	public function get_export_columns() {
		$export_columns = array(
			'code'         => __( 'Coupon code', 'woocommerce' ),
			'orders_count' => __( 'Orders', 'woocommerce' ),
			'amount'       => __( 'Amount discounted', 'woocommerce' ),
			'created'      => __( 'Created', 'woocommerce' ),
			'expires'      => __( 'Expires', 'woocommerce' ),
			'type'         => __( 'Type', 'woocommerce' ),
		);

		/**
		 * Filter to add or remove column names from the coupons report for
		 * export.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_report_coupons_export_columns',
			$export_columns
		);
	}

	/**
	 * Get the column values for export.
	 *
	 * @param array $item Single report item/row.
	 * @return array Key value pair of Column ID => Row Value.
	 */
	public function prepare_item_for_export( $item ) {
		$date_expires = empty( $item['extended_info']['date_expires'] )
			? __( 'N/A', 'woocommerce' )
			: $item['extended_info']['date_expires'];

		$export_item = array(
			'code'         => $item['extended_info']['code'],
			'orders_count' => $item['orders_count'],
			'amount'       => $item['amount'],
			'created'      => $item['extended_info']['date_created'],
			'expires'      => $date_expires,
			'type'         => $item['extended_info']['discount_type'],
		);

		/**
		 * Filter to prepare extra columns in the export item for the coupons
		 * report.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_report_coupons_prepare_export_item',
			$export_item,
			$item
		);
	}
}
Admin/API/Reports/Coupons/Query.php000064400000002246152333011040013103 0ustar00<?php
/**
 * Class for parameter-based Coupons Report querying
 *
 * Example usage:
 * $args = array(
 *          'before'  => '2018-07-19 00:00:00',
 *          'after'   => '2018-07-05 00:00:00',
 *          'page'    => 2,
 *          'coupons' => array(5, 120),
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Coupons\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Coupons;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Coupons\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Products report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array();
	}

	/**
	 * Get product data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_coupons_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-coupons' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_coupons_select_query', $results, $args );
	}

}
Admin/API/Reports/Coupons/Stats/Controller.php000064400000027675152333011040015234 0ustar00<?php
/**
 * REST API Reports coupons stats controller
 *
 * Handles requests to the /reports/coupons/stats endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;

/**
 * REST API Reports coupons stats controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/coupons/stats';


	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args                        = array();
		$args['before']              = $request['before'];
		$args['after']               = $request['after'];
		$args['interval']            = $request['interval'];
		$args['page']                = $request['page'];
		$args['per_page']            = $request['per_page'];
		$args['orderby']             = $request['orderby'];
		$args['order']               = $request['order'];
		$args['coupons']             = (array) $request['coupons'];
		$args['segmentby']           = $request['segmentby'];
		$args['fields']              = $request['fields'];
		$args['force_cache_refresh'] = $request['force_cache_refresh'];

		return $args;
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$query_args    = $this->prepare_reports_query( $request );
		$coupons_query = new Query( $query_args );
		try {
			$report_data = $coupons_query->get_data();
		} catch ( ParameterException $e ) {
			return new \WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}

		$out_data = array(
			'totals'    => get_object_vars( $report_data->totals ),
			'intervals' => array(),
		);

		foreach ( $report_data->intervals as $interval_data ) {
			$item                    = $this->prepare_item_for_response( (object) $interval_data, $request );
			$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $out_data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		$page      = $report_data->page_no;
		$max_pages = $report_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param stdClass        $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = get_object_vars( $report );

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_coupons_stats', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$data_values = array(
			'amount'        => array(
				'description' => __( 'Net discount amount.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
				'format'      => 'currency',
			),
			'coupons_count' => array(
				'description' => __( 'Number of coupons.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'orders_count'  => array(
				'title'       => __( 'Discounted orders', 'woocommerce' ),
				'description' => __( 'Number of discounted orders.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
			),
		);

		$segments = array(
			'segments' => array(
				'description' => __( 'Reports data grouped by segment condition.', 'woocommerce' ),
				'type'        => 'array',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'items'       => array(
					'type'       => 'object',
					'properties' => array(
						'segment_id' => array(
							'description' => __( 'Segment identificator.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'subtotals'  => array(
							'description' => __( 'Interval subtotals.', 'woocommerce' ),
							'type'        => 'object',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
							'properties'  => $data_values,
						),
					),
				),
			),
		);

		$totals = array_merge( $data_values, $segments );

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_coupons_stats',
			'type'       => 'object',
			'properties' => array(
				'totals'    => array(
					'description' => __( 'Totals data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'properties'  => $totals,
				),
				'intervals' => array(
					'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'interval'       => array(
								'description' => __( 'Type of interval.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'enum'        => array( 'day', 'week', 'month', 'year' ),
							),
							'date_start'     => array(
								'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_start_gmt' => array(
								'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end'       => array(
								'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end_gmt'   => array(
								'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'subtotals'      => array(
								'description' => __( 'Interval subtotals.', 'woocommerce' ),
								'type'        => 'object',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'properties'  => $totals,
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params              = array();
		$params['context']   = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']      = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']  = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']     = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']    = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']     = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']   = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'date',
				'amount',
				'coupons_count',
				'orders_count',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['interval']  = array(
			'description'       => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'week',
			'enum'              => array(
				'hour',
				'day',
				'week',
				'month',
				'quarter',
				'year',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['coupons']   = array(
			'description'       => __( 'Limit result set to coupons assigned specific coupon IDs.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['segmentby'] = array(
			'description'       => __( 'Segment the response by additional constraint.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array(
				'product',
				'variation',
				'category',
				'coupon',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['fields']    = array(
			'description'       => __( 'Limit stats fields to the specified items.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
Admin/API/Reports/Coupons/Stats/Segmenter.php000064400000036143152333011040015030 0ustar00<?php
/**
 * Class for adding segmenting support to coupons/stats without cluttering the data store.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Segmenter as ReportsSegmenter;
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;

/**
 * Date & time interval and numeric range handling class for Reporting API.
 */
class Segmenter extends ReportsSegmenter {

	/**
	 * Returns column => query mapping to be used for product-related product-level segmenting query
	 * (e.g. coupon discount amount for product X when segmenting by product id or category).
	 *
	 * @param string $products_table Name of SQL table containing the product-level segmenting info.
	 *
	 * @return array Column => SELECT query mapping.
	 */
	protected function get_segment_selections_product_level( $products_table ) {
		$columns_mapping = array(
			'amount' => "SUM($products_table.coupon_amount) as amount",
		);

		return $columns_mapping;
	}

	/**
	 * Returns column => query mapping to be used for order-related product-level segmenting query
	 * (e.g. orders_count when segmented by category).
	 *
	 * @param string $coupons_lookup_table Name of SQL table containing the order-level segmenting info.
	 *
	 * @return array Column => SELECT query mapping.
	 */
	protected function get_segment_selections_order_level( $coupons_lookup_table ) {
		$columns_mapping = array(
			'coupons_count' => "COUNT(DISTINCT $coupons_lookup_table.coupon_id) as coupons_count",
			'orders_count'  => "COUNT(DISTINCT $coupons_lookup_table.order_id) as orders_count",
		);

		return $columns_mapping;
	}

	/**
	 * Returns column => query mapping to be used for order-level segmenting query
	 * (e.g. discount amount when segmented by coupons).
	 *
	 * @param string $coupons_lookup_table Name of SQL table containing the order-level info.
	 * @param array  $overrides Array of overrides for default column calculations.
	 *
	 * @return array Column => SELECT query mapping.
	 */
	protected function segment_selections_orders( $coupons_lookup_table, $overrides = array() ) {
		$columns_mapping = array(
			'amount'        => "SUM($coupons_lookup_table.discount_amount) as amount",
			'coupons_count' => "COUNT(DISTINCT $coupons_lookup_table.coupon_id) as coupons_count",
			'orders_count'  => "COUNT(DISTINCT $coupons_lookup_table.order_id) as orders_count",
		);

		if ( $overrides ) {
			$columns_mapping = array_merge( $columns_mapping, $overrides );
		}

		return $columns_mapping;
	}

	/**
	 * Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
	 *
	 * @param array  $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $segmenting_dimension_name Name of the segmenting dimension.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $totals_query Array of SQL clauses for totals query.
	 * @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
	 *
	 * @return array
	 */
	protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) {
		global $wpdb;

		// Product-level numbers and order-level numbers can be fetched by the same query.
		$segments_products = $wpdb->get_results(
			"SELECT
						$segmenting_groupby AS $segmenting_dimension_name
						{$segmenting_selections['product_level']}
						{$segmenting_selections['order_level']}
					FROM
						$table_name
						$segmenting_from
						{$totals_query['from_clause']}
					WHERE
						1=1
						{$totals_query['where_time_clause']}
						{$totals_query['where_clause']}
						$segmenting_where
					GROUP BY
						$segmenting_groupby",
			ARRAY_A
		); // WPCS: cache ok, DB call ok, unprepared SQL ok.

		$totals_segments = $this->merge_segment_totals_results( $segmenting_dimension_name, $segments_products, array() );
		return $totals_segments;
	}

	/**
	 * Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
	 *
	 * @param array  $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $segmenting_dimension_name Name of the segmenting dimension.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $intervals_query Array of SQL clauses for intervals query.
	 * @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
	 *
	 * @return array
	 */
	protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) {
		global $wpdb;

		// LIMIT offset, rowcount needs to be updated to LIMIT offset, rowcount * max number of segments.
		$limit_parts      = explode( ',', $intervals_query['limit'] );
		$orig_rowcount    = intval( $limit_parts[1] );
		$segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() );

		// Product-level numbers and order-level numbers can be fetched by the same query.
		$segments_products = $wpdb->get_results(
			"SELECT
						{$intervals_query['select_clause']} AS time_interval,
						$segmenting_groupby AS $segmenting_dimension_name
						{$segmenting_selections['product_level']}
						{$segmenting_selections['order_level']}
					FROM
						$table_name
						$segmenting_from
						{$intervals_query['from_clause']}
					WHERE
						1=1
						{$intervals_query['where_time_clause']}
						{$intervals_query['where_clause']}
						$segmenting_where
					GROUP BY
						time_interval, $segmenting_groupby
					$segmenting_limit",
			ARRAY_A
		); // WPCS: cache ok, DB call ok, unprepared SQL ok.

		$intervals_segments = $this->merge_segment_intervals_results( $segmenting_dimension_name, $segments_products, array() );
		return $intervals_segments;
	}

	/**
	 * Calculate segments for totals query where the segmenting property is bound to order (e.g. coupon or customer type).
	 *
	 * @param string $segmenting_select SELECT part of segmenting SQL query.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $totals_query Array of SQL clauses for intervals query.
	 *
	 * @return array
	 */
	protected function get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $totals_query ) {
		global $wpdb;

		$totals_segments = $wpdb->get_results(
			"SELECT
						$segmenting_groupby
						$segmenting_select
					FROM
						$table_name
						$segmenting_from
						{$totals_query['from_clause']}
					WHERE
						1=1
						{$totals_query['where_time_clause']}
						{$totals_query['where_clause']}
						$segmenting_where
					GROUP BY
						$segmenting_groupby",
			ARRAY_A
		); // WPCS: cache ok, DB call ok, unprepared SQL ok.

		// Reformat result.
		$totals_segments = $this->reformat_totals_segments( $totals_segments, $segmenting_groupby );
		return $totals_segments;
	}

	/**
	 * Calculate segments for intervals query where the segmenting property is bound to order (e.g. coupon or customer type).
	 *
	 * @param string $segmenting_select SELECT part of segmenting SQL query.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $intervals_query Array of SQL clauses for intervals query.
	 *
	 * @return array
	 */
	protected function get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $intervals_query ) {
		global $wpdb;
		$limit_parts      = explode( ',', $intervals_query['limit'] );
		$orig_rowcount    = intval( $limit_parts[1] );
		$segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() );

		$intervals_segments = $wpdb->get_results(
			"SELECT
						MAX($table_name.date_created) AS datetime_anchor,
						{$intervals_query['select_clause']} AS time_interval,
						$segmenting_groupby
						$segmenting_select
					FROM
						$table_name
						$segmenting_from
						{$intervals_query['from_clause']}
					WHERE
						1=1
						{$intervals_query['where_time_clause']}
						{$intervals_query['where_clause']}
						$segmenting_where
					GROUP BY
						time_interval, $segmenting_groupby
					$segmenting_limit",
			ARRAY_A
		); // WPCS: cache ok, DB call ok, unprepared SQL ok.

		// Reformat result.
		$intervals_segments = $this->reformat_intervals_segments( $intervals_segments, $segmenting_groupby );
		return $intervals_segments;
	}

	/**
	 * Return array of segments formatted for REST response.
	 *
	 * @param string $type Type of segments to return--'totals' or 'intervals'.
	 * @param array  $query_params SQL query parameter array.
	 * @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
	 *
	 * @return array
	 * @throws \Automattic\WooCommerce\Admin\API\Reports\ParameterException In case of segmenting by variations, when no parent product is specified.
	 */
	protected function get_segments( $type, $query_params, $table_name ) {
		global $wpdb;
		if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
			return array();
		}

		$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
		$unique_orders_table      = '';
		$segmenting_where         = '';

		// Product, variation, and category are bound to product, so here product segmenting table is required,
		// while coupon and customer are bound to order, so we don't need the extra JOIN for those.
		// This also means that segment selections need to be calculated differently.
		if ( 'product' === $this->query_args['segmentby'] ) {
			$product_level_columns     = $this->get_segment_selections_product_level( $product_segmenting_table );
			$order_level_columns       = $this->get_segment_selections_order_level( $table_name );
			$segmenting_selections     = array(
				'product_level' => $this->prepare_selections( $product_level_columns ),
				'order_level'   => $this->prepare_selections( $order_level_columns ),
			);
			$this->report_columns      = array_merge( $product_level_columns, $order_level_columns );
			$segmenting_from           = "INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)";
			$segmenting_groupby        = $product_segmenting_table . '.product_id';
			$segmenting_dimension_name = 'product_id';

			$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
		} elseif ( 'variation' === $this->query_args['segmentby'] ) {
			if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
				throw new ParameterException( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'woocommerce' ) );
			}

			$product_level_columns     = $this->get_segment_selections_product_level( $product_segmenting_table );
			$order_level_columns       = $this->get_segment_selections_order_level( $table_name );
			$segmenting_selections     = array(
				'product_level' => $this->prepare_selections( $product_level_columns ),
				'order_level'   => $this->prepare_selections( $order_level_columns ),
			);
			$this->report_columns      = array_merge( $product_level_columns, $order_level_columns );
			$segmenting_from           = "INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)";
			$segmenting_where          = "AND $product_segmenting_table.product_id = {$this->query_args['product_includes'][0]}";
			$segmenting_groupby        = $product_segmenting_table . '.variation_id';
			$segmenting_dimension_name = 'variation_id';

			$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
		} elseif ( 'category' === $this->query_args['segmentby'] ) {
			$product_level_columns     = $this->get_segment_selections_product_level( $product_segmenting_table );
			$order_level_columns       = $this->get_segment_selections_order_level( $table_name );
			$segmenting_selections     = array(
				'product_level' => $this->prepare_selections( $product_level_columns ),
				'order_level'   => $this->prepare_selections( $order_level_columns ),
			);
			$this->report_columns      = array_merge( $product_level_columns, $order_level_columns );
			$segmenting_from           = "
			INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)
			LEFT JOIN {$wpdb->term_relationships} ON {$product_segmenting_table}.product_id = {$wpdb->term_relationships}.object_id
			JOIN {$wpdb->term_taxonomy} ON {$wpdb->term_taxonomy}.term_taxonomy_id = {$wpdb->term_relationships}.term_taxonomy_id
			LEFT JOIN {$wpdb->wc_category_lookup} ON {$wpdb->term_taxonomy}.term_id = {$wpdb->wc_category_lookup}.category_id
			";
			$segmenting_where          = " AND {$wpdb->wc_category_lookup}.category_tree_id IS NOT NULL";
			$segmenting_groupby        = "{$wpdb->wc_category_lookup}.category_tree_id";
			$segmenting_dimension_name = 'category_id';

			$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
		} elseif ( 'coupon' === $this->query_args['segmentby'] ) {
			$coupon_level_columns  = $this->segment_selections_orders( $table_name );
			$segmenting_selections = $this->prepare_selections( $coupon_level_columns );
			$this->report_columns  = $coupon_level_columns;
			$segmenting_from       = '';
			$segmenting_groupby    = "$table_name.coupon_id";

			$segments = $this->get_order_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
		}

		return $segments;
	}
}
Admin/API/Reports/Coupons/Stats/Query.php000064400000002305152333011040014175 0ustar00<?php
/**
 * Class for parameter-based Products Report querying
 *
 * Example usage:
 * $args = array(
 *          'before'  => '2018-07-19 00:00:00',
 *          'after'   => '2018-07-05 00:00:00',
 *          'page'    => 2,
 *          'coupons' => array(5, 120),
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Coupons\Stats\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Products report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array();
	}

	/**
	 * Get product data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_coupons_stats_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-coupons-stats' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_coupons_select_query', $results, $args );
	}

}
Admin/API/Reports/Coupons/Stats/DataStore.php000064400000021311152333011040014754 0ustar00<?php
/**
 * API\Reports\Coupons\Stats\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats;

defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;

/**
 * API\Reports\Coupons\Stats\DataStore.
 */
class DataStore extends CouponsDataStore implements DataStoreInterface {
	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'date_start'     => 'strval',
		'date_end'       => 'strval',
		'date_start_gmt' => 'strval',
		'date_end_gmt'   => 'strval',
		'amount'         => 'floatval',
		'coupons_count'  => 'intval',
		'orders_count'   => 'intval',
	);

	/**
	 * SQL columns to select in the db query.
	 *
	 * @var array
	 */
	protected $report_columns;

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'coupons_stats';

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'coupons_stats';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$table_name           = self::get_db_table_name();
		$this->report_columns = array(
			'amount'        => 'SUM(discount_amount) as amount',
			'coupons_count' => 'COUNT(DISTINCT coupon_id) as coupons_count',
			'orders_count'  => "COUNT(DISTINCT {$table_name}.order_id) as orders_count",
		);
	}

	/**
	 * Updates the database query with parameters used for Products Stats report: categories and order status.
	 *
	 * @param array $query_args       Query arguments supplied by the user.
	 */
	protected function update_sql_query_params( $query_args ) {
		global $wpdb;

		$clauses = array(
			'where' => '',
			'join'  => '',
		);

		$order_coupon_lookup_table = self::get_db_table_name();

		$included_coupons = $this->get_included_coupons( $query_args, 'coupons' );
		if ( $included_coupons ) {
			$clauses['where'] .= " AND {$order_coupon_lookup_table}.coupon_id IN ({$included_coupons})";
		}

		$order_status_filter = $this->get_status_subquery( $query_args );
		if ( $order_status_filter ) {
			$clauses['join']  .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_coupon_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
			$clauses['where'] .= " AND ( {$order_status_filter} )";
		}

		$this->add_time_period_sql_params( $query_args, $order_coupon_lookup_table );
		$this->add_intervals_sql_params( $query_args, $order_coupon_lookup_table );
		$clauses['where_time'] = $this->get_sql_clause( 'where_time' );

		$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
		$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
		$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) );
		$this->interval_query->add_sql_clause( 'select', 'AS time_interval' );

		foreach ( array( 'join', 'where_time', 'where' ) as $clause ) {
			$this->interval_query->add_sql_clause( $clause, $clauses[ $clause ] );
			$this->total_query->add_sql_clause( $clause, $clauses[ $clause ] );
		}
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @since 3.5.0
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page' => get_option( 'posts_per_page' ),
			'page'     => 1,
			'order'    => 'DESC',
			'orderby'  => 'date',
			'before'   => TimeInterval::default_before(),
			'after'    => TimeInterval::default_after(),
			'fields'   => '*',
			'interval' => 'week',
			'coupons'  => array(),
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$data = (object) array(
				'data'    => array(),
				'total'   => 0,
				'pages'   => 0,
				'page_no' => 0,
			);

			$selections      = $this->selected_columns( $query_args );
			$totals_query    = array();
			$intervals_query = array();
			$limit_params    = $this->get_limit_sql_params( $query_args );
			$this->update_sql_query_params( $query_args, $totals_query, $intervals_query );

			$db_intervals = $wpdb->get_col(
				$this->interval_query->get_query_statement()
			); // WPCS: cache ok, DB call ok, unprepared SQL ok.

			$db_interval_count       = count( $db_intervals );
			$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
			$total_pages             = (int) ceil( $expected_interval_count / $limit_params['per_page'] );
			if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
				return $data;
			}

			$this->total_query->add_sql_clause( 'select', $selections );
			$totals = $wpdb->get_results(
				$this->total_query->get_query_statement(),
				ARRAY_A
			); // WPCS: cache ok, DB call ok, unprepared SQL ok.

			if ( null === $totals ) {
				return $data;
			}

			// @todo remove these assignements when refactoring segmenter classes to use query objects.
			$totals_query          = array(
				'from_clause'       => $this->total_query->get_sql_clause( 'join' ),
				'where_time_clause' => $this->total_query->get_sql_clause( 'where_time' ),
				'where_clause'      => $this->total_query->get_sql_clause( 'where' ),
			);
			$intervals_query       = array(
				'select_clause'     => $this->get_sql_clause( 'select' ),
				'from_clause'       => $this->interval_query->get_sql_clause( 'join' ),
				'where_time_clause' => $this->interval_query->get_sql_clause( 'where_time' ),
				'where_clause'      => $this->interval_query->get_sql_clause( 'where' ),
				'limit'             => $this->get_sql_clause( 'limit' ),
			);
			$segmenter             = new Segmenter( $query_args, $this->report_columns );
			$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
			$totals                = (object) $this->cast_numbers( $totals[0] );

			// Intervals.
			$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
			$this->interval_query->add_sql_clause( 'select', ", MAX({$table_name}.date_created) AS datetime_anchor" );

			if ( '' !== $selections ) {
				$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
			}

			$intervals = $wpdb->get_results(
				$this->interval_query->get_query_statement(),
				ARRAY_A
			); // WPCS: cache ok, DB call ok, unprepared SQL ok.

			if ( null === $intervals ) {
				return $data;
			}

			$data = (object) array(
				'totals'    => $totals,
				'intervals' => $intervals,
				'total'     => $expected_interval_count,
				'pages'     => $total_pages,
				'page_no'   => (int) $query_args['page'],
			);

			if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $limit_params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
				$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
				$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
				$this->remove_extra_records( $data, $query_args['page'], $limit_params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
			} else {
				$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
			}
			$segmenter->add_intervals_segments( $data, $intervals_query, $table_name );
			$this->create_interval_subtotals( $data->intervals );

			$this->set_cached_data( $cache_key, $data );
		}

		return $data;
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		unset( $this->subquery );
		$this->total_query = new SqlQuery( $this->context . '_total' );
		$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );

		$this->interval_query = new SqlQuery( $this->context . '_interval' );
		$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
		$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
	}
}
Admin/API/Reports/Products/Query.php000064400000002353152333011040013257 0ustar00<?php
/**
 * Class for parameter-based Products Report querying
 *
 * Example usage:
 * $args = array(
 *          'before'       => '2018-07-19 00:00:00',
 *          'after'        => '2018-07-05 00:00:00',
 *          'page'         => 2,
 *          'categories'   => array(15, 18),
 *          'products'     => array(1,2,3)
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Products\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Products;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Products\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Products report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array();
	}

	/**
	 * Get product data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_products_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-products' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_products_select_query', $results, $args );
	}
}
Admin/API/Reports/Products/DataStore.php000064400000042533152333011040014044 0ustar00<?php
/**
 * API\Reports\Products\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Products;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;

/**
 * API\Reports\Products\DataStore.
 */
class DataStore extends ReportsDataStore implements DataStoreInterface {

	/**
	 * Table used to get the data.
	 *
	 * @var string
	 */
	protected static $table_name = 'wc_order_product_lookup';

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'products';

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'date_start'       => 'strval',
		'date_end'         => 'strval',
		'product_id'       => 'intval',
		'items_sold'       => 'intval',
		'net_revenue'      => 'floatval',
		'orders_count'     => 'intval',
		// Extended attributes.
		'name'             => 'strval',
		'price'            => 'floatval',
		'image'            => 'strval',
		'permalink'        => 'strval',
		'stock_status'     => 'strval',
		'stock_quantity'   => 'intval',
		'low_stock_amount' => 'intval',
		'category_ids'     => 'array_values',
		'variations'       => 'array_values',
		'sku'              => 'strval',
	);

	/**
	 * Extended product attributes to include in the data.
	 *
	 * @var array
	 */
	protected $extended_attributes = array(
		'name',
		'price',
		'image',
		'permalink',
		'stock_status',
		'stock_quantity',
		'manage_stock',
		'low_stock_amount',
		'category_ids',
		'variations',
		'sku',
	);

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'products';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$table_name           = self::get_db_table_name();
		$this->report_columns = array(
			'product_id'   => 'product_id',
			'items_sold'   => 'SUM(product_qty) as items_sold',
			'net_revenue'  => 'SUM(product_net_revenue) AS net_revenue',
			'orders_count' => "COUNT( DISTINCT ( CASE WHEN product_gross_revenue >= 0 THEN {$table_name}.order_id END ) ) as orders_count",
		);
	}

	/**
	 * Set up all the hooks for maintaining and populating table data.
	 */
	public static function init() {
		add_action( 'woocommerce_analytics_delete_order_stats', array( __CLASS__, 'sync_on_order_delete' ), 10 );
	}

	/**
	 * Fills FROM clause of SQL request based on user supplied parameters.
	 *
	 * @param array  $query_args Parameters supplied by the user.
	 * @param string $arg_name   Target of the JOIN sql param.
	 * @param string $id_cell    ID cell identifier, like `table_name.id_column_name`.
	 */
	protected function add_from_sql_params( $query_args, $arg_name, $id_cell ) {
		global $wpdb;

		$type = 'join';
		// Order by product name requires extra JOIN.
		switch ( $query_args['orderby'] ) {
			case 'product_name':
				$join = " JOIN {$wpdb->posts} AS _products ON {$id_cell} = _products.ID";
				break;
			case 'sku':
				$join = " LEFT JOIN {$wpdb->postmeta} AS postmeta ON {$id_cell} = postmeta.post_id AND postmeta.meta_key = '_sku'";
				break;
			case 'variations':
				$type = 'left_join';
				$join = "LEFT JOIN ( SELECT post_parent, COUNT(*) AS variations FROM {$wpdb->posts} WHERE post_type = 'product_variation' GROUP BY post_parent ) AS _variations ON {$id_cell} = _variations.post_parent";
				break;
			default:
				$join = '';
				break;
		}
		if ( $join ) {
			if ( 'inner' === $arg_name ) {
				$this->subquery->add_sql_clause( $type, $join );
			} else {
				$this->add_sql_clause( $type, $join );
			}
		}
	}

	/**
	 * Updates the database query with parameters used for Products report: categories and order status.
	 *
	 * @param array $query_args Query arguments supplied by the user.
	 */
	protected function add_sql_query_params( $query_args ) {
		global $wpdb;
		$order_product_lookup_table = self::get_db_table_name();

		$this->add_time_period_sql_params( $query_args, $order_product_lookup_table );
		$this->get_limit_sql_params( $query_args );
		$this->add_order_by_sql_params( $query_args );

		$included_products = $this->get_included_products( $query_args );
		if ( $included_products ) {
			$this->add_from_sql_params( $query_args, 'outer', 'default_results.product_id' );
			$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" );
		} else {
			$this->add_from_sql_params( $query_args, 'inner', "{$order_product_lookup_table}.product_id" );
		}

		$included_variations = $this->get_included_variations( $query_args );
		if ( $included_variations ) {
			$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id IN ({$included_variations})" );
		}

		$order_status_filter = $this->get_status_subquery( $query_args );
		if ( $order_status_filter ) {
			$this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id" );
			$this->subquery->add_sql_clause( 'where', "AND ( {$order_status_filter} )" );
		}
	}

	/**
	 * Maps ordering specified by the user to columns in the database/fields in the data.
	 *
	 * @param string $order_by Sorting criterion.
	 * @return string
	 */
	protected function normalize_order_by( $order_by ) {
		if ( 'date' === $order_by ) {
			return self::get_db_table_name() . '.date_created';
		}
		if ( 'product_name' === $order_by ) {
			return 'post_title';
		}
		if ( 'sku' === $order_by ) {
			return 'meta_value';
		}
		return $order_by;
	}

	/**
	 * Enriches the product data with attributes specified by the extended_attributes.
	 *
	 * @param array $products_data Product data.
	 * @param array $query_args  Query parameters.
	 */
	protected function include_extended_info( &$products_data, $query_args ) {
		global $wpdb;
		$product_names = array();

		foreach ( $products_data as $key => $product_data ) {
			$extended_info = new \ArrayObject();
			if ( $query_args['extended_info'] ) {
				$product_id = $product_data['product_id'];
				$product    = wc_get_product( $product_id );
				// Product was deleted.
				if ( ! $product ) {
					if ( ! isset( $product_names[ $product_id ] ) ) {
						$product_names[ $product_id ] = $wpdb->get_var(
							$wpdb->prepare(
								"SELECT i.order_item_name
								FROM {$wpdb->prefix}woocommerce_order_items i, {$wpdb->prefix}woocommerce_order_itemmeta m
								WHERE i.order_item_id = m.order_item_id
								AND m.meta_key = '_product_id'
								AND m.meta_value = %s
								ORDER BY i.order_item_id DESC
								LIMIT 1",
								$product_id
							)
						);
					}

					/* translators: %s is product name */
					$products_data[ $key ]['extended_info']['name'] = $product_names[ $product_id ] ? sprintf( __( '%s (Deleted)', 'woocommerce' ), $product_names[ $product_id ] ) : __( '(Deleted)', 'woocommerce' );
					continue;
				}

				$extended_attributes = apply_filters( 'woocommerce_rest_reports_products_extended_attributes', $this->extended_attributes, $product_data );
				foreach ( $extended_attributes as $extended_attribute ) {
					if ( 'variations' === $extended_attribute ) {
						if ( ! $product->is_type( 'variable' ) ) {
							continue;
						}
						$function = 'get_children';
					} else {
						$function = 'get_' . $extended_attribute;
					}
					if ( is_callable( array( $product, $function ) ) ) {
						$value                                = $product->{$function}();
						$extended_info[ $extended_attribute ] = $value;
					}
				}
				// If there is no set low_stock_amount, use the one in user settings.
				if ( '' === $extended_info['low_stock_amount'] ) {
					$extended_info['low_stock_amount'] = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
				}
				$extended_info = $this->cast_numbers( $extended_info );
			}
			$products_data[ $key ]['extended_info'] = $extended_info;
		}
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page'          => get_option( 'posts_per_page' ),
			'page'              => 1,
			'order'             => 'DESC',
			'orderby'           => 'date',
			'before'            => TimeInterval::default_before(),
			'after'             => TimeInterval::default_after(),
			'fields'            => '*',
			'category_includes' => array(),
			'product_includes'  => array(),
			'extended_info'     => false,
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$data = (object) array(
				'data'    => array(),
				'total'   => 0,
				'pages'   => 0,
				'page_no' => 0,
			);

			$selections        = $this->selected_columns( $query_args );
			$included_products = $this->get_included_products_array( $query_args );
			$params            = $this->get_limit_params( $query_args );
			$this->add_sql_query_params( $query_args );

			if ( count( $included_products ) > 0 ) {
				$filtered_products = array_diff( $included_products, array( '-1' ) );
				$total_results     = count( $filtered_products );
				$total_pages       = (int) ceil( $total_results / $params['per_page'] );

				if ( 'date' === $query_args['orderby'] ) {
					$selections .= ", {$table_name}.date_created";
				}

				$fields          = $this->get_fields( $query_args );
				$join_selections = $this->format_join_selections( $fields, array( 'product_id' ) );
				$ids_table       = $this->get_ids_table( $included_products, 'product_id' );

				$this->subquery->clear_sql_clause( 'select' );
				$this->subquery->add_sql_clause( 'select', $selections );
				$this->add_sql_clause( 'select', $join_selections );
				$this->add_sql_clause( 'from', '(' );
				$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
				$this->add_sql_clause( 'from', ") AS {$table_name}" );
				$this->add_sql_clause(
					'right_join',
					"RIGHT JOIN ( {$ids_table} ) AS default_results
					ON default_results.product_id = {$table_name}.product_id"
				);
				$this->add_sql_clause( 'where', 'AND default_results.product_id != -1' );

				$products_query = $this->get_query_statement();
			} else {
				$count_query      = "SELECT COUNT(*) FROM (
						{$this->subquery->get_query_statement()}
					) AS tt";
				$db_records_count = (int) $wpdb->get_var(
					$count_query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				);

				$total_results = $db_records_count;
				$total_pages   = (int) ceil( $db_records_count / $params['per_page'] );

				if ( ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) ) {
					return $data;
				}

				$this->subquery->clear_sql_clause( 'select' );
				$this->subquery->add_sql_clause( 'select', $selections );
				$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
				$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
				$products_query = $this->subquery->get_query_statement();
			}

			$product_data = $wpdb->get_results(
				$products_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				ARRAY_A
			);

			if ( null === $product_data ) {
				return $data;
			}

			$product_data = array_map( array( $this, 'cast_numbers' ), $product_data );
			$data         = (object) array(
				'data'    => $product_data,
				'total'   => $total_results,
				'pages'   => $total_pages,
				'page_no' => (int) $query_args['page'],
			);

			$this->set_cached_data( $cache_key, $data );
		}

		$this->include_extended_info( $data->data, $query_args );

		return $data;
	}

	/**
	 * Create or update an entry in the wc_admin_order_product_lookup table for an order.
	 *
	 * @since 3.5.0
	 * @param int $order_id Order ID.
	 * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
	 */
	public static function sync_order_products( $order_id ) {
		global $wpdb;

		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return -1;
		}

		$table_name     = self::get_db_table_name();
		$existing_items = $wpdb->get_col(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT order_item_id FROM {$table_name} WHERE order_id = %d",
				$order_id
			)
		);
		$existing_items = array_flip( $existing_items );
		$order_items    = $order->get_items();
		$num_updated    = 0;
		$decimals       = wc_get_price_decimals();
		$round_tax      = 'no' === get_option( 'woocommerce_tax_round_at_subtotal' );

		foreach ( $order_items as $order_item ) {
			$order_item_id = $order_item->get_id();
			unset( $existing_items[ $order_item_id ] );
			$product_qty         = $order_item->get_quantity( 'edit' );
			$shipping_amount     = $order->get_item_shipping_amount( $order_item );
			$shipping_tax_amount = $order->get_item_shipping_tax_amount( $order_item );
			$coupon_amount       = $order->get_item_coupon_amount( $order_item );

			// Skip line items without changes to product quantity.
			if ( ! $product_qty ) {
				$num_updated++;
				continue;
			}

			// Tax amount.
			$tax_amount  = 0;
			$order_taxes = $order->get_taxes();
			$tax_data    = $order_item->get_taxes();
			foreach ( $order_taxes as $tax_item ) {
				$tax_item_id = $tax_item->get_rate_id();
				$tax_amount += isset( $tax_data['total'][ $tax_item_id ] ) ? (float) $tax_data['total'][ $tax_item_id ] : 0;
			}

			$net_revenue = round( $order_item->get_total( 'edit' ), $decimals );
			if ( $round_tax ) {
				$tax_amount = round( $tax_amount, $decimals );
			}

			$result = $wpdb->replace(
				self::get_db_table_name(),
				array(
					'order_item_id'         => $order_item_id,
					'order_id'              => $order->get_id(),
					'product_id'            => wc_get_order_item_meta( $order_item_id, '_product_id' ),
					'variation_id'          => wc_get_order_item_meta( $order_item_id, '_variation_id' ),
					'customer_id'           => $order->get_report_customer_id(),
					'product_qty'           => $product_qty,
					'product_net_revenue'   => $net_revenue,
					'date_created'          => $order->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ),
					'coupon_amount'         => $coupon_amount,
					'tax_amount'            => $tax_amount,
					'shipping_amount'       => $shipping_amount,
					'shipping_tax_amount'   => $shipping_tax_amount,
					// @todo Can this be incorrect if modified by filters?
					'product_gross_revenue' => $net_revenue + $tax_amount + $shipping_amount + $shipping_tax_amount,
				),
				array(
					'%d', // order_item_id.
					'%d', // order_id.
					'%d', // product_id.
					'%d', // variation_id.
					'%d', // customer_id.
					'%d', // product_qty.
					'%f', // product_net_revenue.
					'%s', // date_created.
					'%f', // coupon_amount.
					'%f', // tax_amount.
					'%f', // shipping_amount.
					'%f', // shipping_tax_amount.
					'%f', // product_gross_revenue.
				)
			); // WPCS: cache ok, DB call ok, unprepared SQL ok.

			/**
			 * Fires when product's reports are updated.
			 *
			 * @param int $order_item_id Order Item ID.
			 * @param int $order_id      Order ID.
			 */
			do_action( 'woocommerce_analytics_update_product', $order_item_id, $order->get_id() );

			// Sum the rows affected. Using REPLACE can affect 2 rows if the row already exists.
			$num_updated += 2 === intval( $result ) ? 1 : intval( $result );
		}

		if ( ! empty( $existing_items ) ) {
			$existing_items = array_flip( $existing_items );
			$format         = array_fill( 0, count( $existing_items ), '%d' );
			$format         = implode( ',', $format );
			array_unshift( $existing_items, $order_id );
			$wpdb->query(
				$wpdb->prepare(
					// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					"DELETE FROM {$table_name} WHERE order_id = %d AND order_item_id in ({$format})",
					$existing_items
				)
			);
		}

		return ( count( $order_items ) === $num_updated );
	}

	/**
	 * Clean products data when an order is deleted.
	 *
	 * @param int $order_id Order ID.
	 */
	public static function sync_on_order_delete( $order_id ) {
		global $wpdb;

		$wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) );

		/**
		 * Fires when product's reports are removed from database.
		 *
		 * @param int $product_id Product ID.
		 * @param int $order_id   Order ID.
		 */
		do_action( 'woocommerce_analytics_delete_product', 0, $order_id );

		ReportsCache::invalidate();
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		$this->subquery = new SqlQuery( $this->context . '_subquery' );
		$this->subquery->add_sql_clause( 'select', 'product_id' );
		$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
		$this->subquery->add_sql_clause( 'group_by', 'product_id' );
	}
}
Admin/API/Reports/Products/Controller.php000064400000034660152333011040014303 0ustar00<?php
/**
 * REST API Reports products controller
 *
 * Handles requests to the /reports/products endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Products;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;

/**
 * REST API Reports products controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/products';

	/**
	 * Mapping between external parameter name and name used in query class.
	 *
	 * @var array
	 */
	protected $param_mapping = array(
		'categories' => 'category_includes',
		'products'   => 'product_includes',
		'variations' => 'variation_includes',
	);

	/**
	 * Get items.
	 *
	 * @param WP_REST_Request $request Request data.
	 *
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$args       = array();
		$registered = array_keys( $this->get_collection_params() );
		foreach ( $registered as $param_name ) {
			if ( isset( $request[ $param_name ] ) ) {
				if ( isset( $this->param_mapping[ $param_name ] ) ) {
					$args[ $this->param_mapping[ $param_name ] ] = $request[ $param_name ];
				} else {
					$args[ $param_name ] = $request[ $param_name ];
				}
			}
		}

		$reports       = new Query( $args );
		$products_data = $reports->get_data();

		$data = array();

		foreach ( $products_data->data as $product_data ) {
			$item = $this->prepare_item_for_response( $product_data, $request );
			if ( isset( $item->data['extended_info']['name'] ) ) {
				$item->data['extended_info']['name'] = wp_strip_all_tags( $item->data['extended_info']['name'] );
			}
			$data[] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $data );
		$response->header( 'X-WP-Total', (int) $products_data->total );
		$response->header( 'X-WP-TotalPages', (int) $products_data->pages );

		$page      = $products_data->page_no;
		$max_pages = $products_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param Array           $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $report ) );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_products', $response, $report, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param Array $object Object data.
	 * @return array        Links for the given post.
	 */
	protected function prepare_links( $object ) {
		$links = array(
			'product' => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, 'products', $object['product_id'] ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_products',
			'type'       => 'object',
			'properties' => array(
				'product_id'    => array(
					'type'        => 'integer',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Product ID.', 'woocommerce' ),
				),
				'items_sold'    => array(
					'type'        => 'integer',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Number of items sold.', 'woocommerce' ),
				),
				'net_revenue'   => array(
					'type'        => 'number',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Total Net sales of all items sold.', 'woocommerce' ),
				),
				'orders_count'  => array(
					'type'        => 'integer',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'description' => __( 'Number of orders product appeared in.', 'woocommerce' ),
				),
				'extended_info' => array(
					'name'             => array(
						'type'        => 'string',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product name.', 'woocommerce' ),
					),
					'price'            => array(
						'type'        => 'number',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product price.', 'woocommerce' ),
					),
					'image'            => array(
						'type'        => 'string',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product image.', 'woocommerce' ),
					),
					'permalink'        => array(
						'type'        => 'string',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product link.', 'woocommerce' ),
					),
					'category_ids'     => array(
						'type'        => 'array',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product category IDs.', 'woocommerce' ),
					),
					'stock_status'     => array(
						'type'        => 'string',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product inventory status.', 'woocommerce' ),
					),
					'stock_quantity'   => array(
						'type'        => 'integer',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product inventory quantity.', 'woocommerce' ),
					),
					'low_stock_amount' => array(
						'type'        => 'integer',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product inventory threshold for low stock.', 'woocommerce' ),
					),
					'variations'       => array(
						'type'        => 'array',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product variations IDs.', 'woocommerce' ),
					),
					'sku'              => array(
						'type'        => 'string',
						'readonly'    => true,
						'context'     => array( 'view', 'edit' ),
						'description' => __( 'Product SKU.', 'woocommerce' ),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params               = array();
		$params['context']    = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']       = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']   = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']      = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']     = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']      = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']    = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'date',
				'net_revenue',
				'orders_count',
				'items_sold',
				'product_name',
				'variations',
				'sku',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['categories'] = array(
			'description'       => __( 'Limit result to items from the specified categories.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['match']      = array(
			'description'       => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'all',
			'enum'              => array(
				'all',
				'any',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['products']   = array(
			'description'       => __( 'Limit result to items with specified product ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),

		);
		$params['extended_info'] = array(
			'description'       => __( 'Add additional piece of info about each product to the report.', 'woocommerce' ),
			'type'              => 'boolean',
			'default'           => false,
			'sanitize_callback' => 'wc_string_to_bool',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get stock status column export value.
	 *
	 * @param array $status Stock status from report row.
	 * @return string
	 */
	protected function get_stock_status( $status ) {
		$statuses = wc_get_product_stock_status_options();

		return isset( $statuses[ $status ] ) ? $statuses[ $status ] : '';
	}

	/**
	 * Get categories column export value.
	 *
	 * @param array $category_ids Category IDs from report row.
	 * @return string
	 */
	protected function get_categories( $category_ids ) {
		$category_names = get_terms(
			array(
				'taxonomy' => 'product_cat',
				'include'  => $category_ids,
				'fields'   => 'names',
			)
		);

		return implode( ', ', $category_names );
	}

	/**
	 * Get the column names for export.
	 *
	 * @return array Key value pair of Column ID => Label.
	 */
	public function get_export_columns() {
		$export_columns = array(
			'product_name' => __( 'Product title', 'woocommerce' ),
			'sku'          => __( 'SKU', 'woocommerce' ),
			'items_sold'   => __( 'Items sold', 'woocommerce' ),
			'net_revenue'  => __( 'N. Revenue', 'woocommerce' ),
			'orders_count' => __( 'Orders', 'woocommerce' ),
			'product_cat'  => __( 'Category', 'woocommerce' ),
			'variations'   => __( 'Variations', 'woocommerce' ),
		);

		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			$export_columns['stock_status'] = __( 'Status', 'woocommerce' );
			$export_columns['stock']        = __( 'Stock', 'woocommerce' );
		}

		/**
		 * Filter to add or remove column names from the products report for
		 * export.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_report_products_export_columns',
			$export_columns
		);
	}

	/**
	 * Get the column values for export.
	 *
	 * @param array $item Single report item/row.
	 * @return array Key value pair of Column ID => Row Value.
	 */
	public function prepare_item_for_export( $item ) {
		$export_item = array(
			'product_name' => $item['extended_info']['name'],
			'sku'          => $item['extended_info']['sku'],
			'items_sold'   => $item['items_sold'],
			'net_revenue'  => $item['net_revenue'],
			'orders_count' => $item['orders_count'],
			'product_cat'  => $this->get_categories( $item['extended_info']['category_ids'] ),
			'variations'   => isset( $item['extended_info']['variations'] ) ? count( $item['extended_info']['variations'] ) : 0,
		);

		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			if ( $item['extended_info']['manage_stock'] ) {
				$export_item['stock_status'] = $this->get_stock_status( $item['extended_info']['stock_status'] );
				$export_item['stock']        = $item['extended_info']['stock_quantity'];
			} else {
				$export_item['stock_status'] = __( 'N/A', 'woocommerce' );
				$export_item['stock']        = __( 'N/A', 'woocommerce' );
			}
		}

		/**
		 * Filter to prepare extra columns in the export item for the products
		 * report.
		 *
		 * @since 1.6.0
		 */
		return apply_filters(
			'woocommerce_report_products_prepare_export_item',
			$export_item,
			$item
		);
	}
}
Admin/API/Reports/Products/Stats/Controller.php000064400000033161152333011040015374 0ustar00<?php
/**
 * REST API Reports products stats controller
 *
 * Handles requests to the /reports/products/stats endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Products\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;

/**
 * REST API Reports products stats controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/products/stats';

	/**
	 * Mapping between external parameter name and name used in query class.
	 *
	 * @var array
	 */
	protected $param_mapping = array(
		'categories' => 'category_includes',
		'products'   => 'product_includes',
		'variations' => 'variation_includes',
	);

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_filter( 'woocommerce_analytics_products_stats_select_query', array( $this, 'set_default_report_data' ) );
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$query_args = array(
			'fields' => array(
				'items_sold',
				'net_revenue',
				'orders_count',
				'products_count',
				'variations_count',
			),
		);

		$registered = array_keys( $this->get_collection_params() );
		foreach ( $registered as $param_name ) {
			if ( isset( $request[ $param_name ] ) ) {
				if ( isset( $this->param_mapping[ $param_name ] ) ) {
					$query_args[ $this->param_mapping[ $param_name ] ] = $request[ $param_name ];
				} else {
					$query_args[ $param_name ] = $request[ $param_name ];
				}
			}
		}

		$query = new Query( $query_args );
		try {
			$report_data = $query->get_data();
		} catch ( ParameterException $e ) {
			return new \WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}

		$out_data = array(
			'totals'    => get_object_vars( $report_data->totals ),
			'intervals' => array(),
		);

		foreach ( $report_data->intervals as $interval_data ) {
			$item                    = $this->prepare_item_for_response( $interval_data, $request );
			$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $out_data );
		$response->header( 'X-WP-Total', (int) $report_data->total );
		$response->header( 'X-WP-TotalPages', (int) $report_data->pages );

		$page      = $report_data->page_no;
		$max_pages = $report_data->pages;
		$base      = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param Array           $report  Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = $report;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_products_stats', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$data_values = array(
			'items_sold'   => array(
				'title'       => __( 'Products sold', 'woocommerce' ),
				'description' => __( 'Number of product items sold.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'indicator'   => true,
			),
			'net_revenue'  => array(
				'description' => __( 'Net sales.', 'woocommerce' ),
				'type'        => 'number',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'format'      => 'currency',
			),
			'orders_count' => array(
				'description' => __( 'Number of orders.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
		);

		$segments = array(
			'segments' => array(
				'description' => __( 'Reports data grouped by segment condition.', 'woocommerce' ),
				'type'        => 'array',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'items'       => array(
					'type'       => 'object',
					'properties' => array(
						'segment_id'    => array(
							'description' => __( 'Segment identificator.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'segment_label' => array(
							'description' => __( 'Human readable segment label, either product or variation name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
							'enum'        => array( 'day', 'week', 'month', 'year' ),
						),
						'subtotals'     => array(
							'description' => __( 'Interval subtotals.', 'woocommerce' ),
							'type'        => 'object',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
							'properties'  => $data_values,
						),
					),
				),
			),
		);

		$totals = array_merge( $data_values, $segments );

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_products_stats',
			'type'       => 'object',
			'properties' => array(
				'totals'    => array(
					'description' => __( 'Totals data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'properties'  => $totals,
				),
				'intervals' => array(
					'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'interval'       => array(
								'description' => __( 'Type of interval.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'enum'        => array( 'day', 'week', 'month', 'year' ),
							),
							'date_start'     => array(
								'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_start_gmt' => array(
								'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end'       => array(
								'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_end_gmt'   => array(
								'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'subtotals'      => array(
								'description' => __( 'Interval subtotals.', 'woocommerce' ),
								'type'        => 'object',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'properties'  => $totals,
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Set the default results to 0 if API returns an empty array
	 *
	 * @internal
	 * @param Mixed $results Report data.
	 * @return object
	 */
	public function set_default_report_data( $results ) {
		if ( empty( $results ) ) {
			$results                       = new \stdClass();
			$results->total                = 0;
			$results->totals               = new \stdClass();
			$results->totals->items_sold   = 0;
			$results->totals->net_revenue  = 0;
			$results->totals->orders_count = 0;
			$results->intervals            = array();
			$results->pages                = 1;
			$results->page_no              = 1;
		}
		return $results;
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params               = array();
		$params['context']    = $this->get_context_param( array( 'default' => 'view' ) );
		$params['page']       = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']   = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']      = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']     = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']      = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']    = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'date',
				'net_revenue',
				'coupons',
				'refunds',
				'shipping',
				'taxes',
				'net_revenue',
				'orders_count',
				'items_sold',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['interval']   = array(
			'description'       => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'week',
			'enum'              => array(
				'hour',
				'day',
				'week',
				'month',
				'quarter',
				'year',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['categories'] = array(
			'description'       => __( 'Limit result to items from the specified categories.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['products']   = array(
			'description'       => __( 'Limit result to items with specified product ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['variations'] = array(
			'description'       => __( 'Limit result to items with specified variation ids.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'integer',
			),
		);
		$params['segmentby']  = array(
			'description'       => __( 'Segment the response by additional constraint.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array(
				'product',
				'category',
				'variation',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['fields']     = array(
			'description'       => __( 'Limit stats fields to the specified items.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
			),
		);
		$params['force_cache_refresh'] = array(
			'description'       => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wp_validate_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
Admin/API/Reports/Products/Stats/DataStore.php000064400000023377152333011040015147 0ustar00<?php
/**
 * API\Reports\Products\Stats\DataStore class file.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Products\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;

/**
 * API\Reports\Products\Stats\DataStore.
 */
class DataStore extends ProductsDataStore implements DataStoreInterface {

	/**
	 * Mapping columns to data type to return correct response types.
	 *
	 * @var array
	 */
	protected $column_types = array(
		'date_start'       => 'strval',
		'date_end'         => 'strval',
		'product_id'       => 'intval',
		'items_sold'       => 'intval',
		'net_revenue'      => 'floatval',
		'orders_count'     => 'intval',
		'products_count'   => 'intval',
		'variations_count' => 'intval',
	);

	/**
	 * Cache identifier.
	 *
	 * @var string
	 */
	protected $cache_key = 'products_stats';

	/**
	 * Data store context used to pass to filters.
	 *
	 * @var string
	 */
	protected $context = 'products_stats';

	/**
	 * Assign report columns once full table name has been assigned.
	 */
	protected function assign_report_columns() {
		$table_name           = self::get_db_table_name();
		$this->report_columns = array(
			'items_sold'       => 'SUM(product_qty) as items_sold',
			'net_revenue'      => 'SUM(product_net_revenue) AS net_revenue',
			'orders_count'     => "COUNT( DISTINCT ( CASE WHEN product_gross_revenue >= 0 THEN {$table_name}.order_id END ) ) as orders_count",
			'products_count'   => 'COUNT(DISTINCT product_id) as products_count',
			'variations_count' => 'COUNT(DISTINCT variation_id) as variations_count',
		);
	}

	/**
	 * Updates the database query with parameters used for Products Stats report: categories and order status.
	 *
	 * @param array $query_args       Query arguments supplied by the user.
	 */
	protected function update_sql_query_params( $query_args ) {
		global $wpdb;

		$products_where_clause      = '';
		$products_from_clause       = '';
		$order_product_lookup_table = self::get_db_table_name();

		$included_products = $this->get_included_products( $query_args );
		if ( $included_products ) {
			$products_where_clause .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";
		}

		$included_variations = $this->get_included_variations( $query_args );
		if ( $included_variations ) {
			$products_where_clause .= " AND {$order_product_lookup_table}.variation_id IN ({$included_variations})";
		}

		$order_status_filter = $this->get_status_subquery( $query_args );
		if ( $order_status_filter ) {
			$products_from_clause  .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
			$products_where_clause .= " AND ( {$order_status_filter} )";
		}

		$this->add_time_period_sql_params( $query_args, $order_product_lookup_table );
		$this->total_query->add_sql_clause( 'where', $products_where_clause );
		$this->total_query->add_sql_clause( 'join', $products_from_clause );

		$this->add_intervals_sql_params( $query_args, $order_product_lookup_table );
		$this->interval_query->add_sql_clause( 'where', $products_where_clause );
		$this->interval_query->add_sql_clause( 'join', $products_from_clause );
		$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
	}

	/**
	 * Returns the report data based on parameters supplied by the user.
	 *
	 * @since 3.5.0
	 * @param array $query_args  Query parameters.
	 * @return stdClass|WP_Error Data.
	 */
	public function get_data( $query_args ) {
		global $wpdb;

		$table_name = self::get_db_table_name();

		// These defaults are only partially applied when used via REST API, as that has its own defaults.
		$defaults   = array(
			'per_page'          => get_option( 'posts_per_page' ),
			'page'              => 1,
			'order'             => 'DESC',
			'orderby'           => 'date',
			'before'            => TimeInterval::default_before(),
			'after'             => TimeInterval::default_after(),
			'fields'            => '*',
			'category_includes' => array(),
			'interval'          => 'week',
			'product_includes'  => array(),
		);
		$query_args = wp_parse_args( $query_args, $defaults );
		$this->normalize_timezones( $query_args, $defaults );

		/*
		 * We need to get the cache key here because
		 * parent::update_intervals_sql_params() modifies $query_args.
		 */
		$cache_key = $this->get_cache_key( $query_args );
		$data      = $this->get_cached_data( $cache_key );

		if ( false === $data ) {
			$this->initialize_queries();

			$selections = $this->selected_columns( $query_args );
			$params     = $this->get_limit_params( $query_args );

			$this->update_sql_query_params( $query_args );
			$this->get_limit_sql_params( $query_args );
			$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );

			$db_intervals = $wpdb->get_col(
				$this->interval_query->get_query_statement()
			); // WPCS: cache ok, DB call ok, unprepared SQL ok.

			$db_interval_count       = count( $db_intervals );
			$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
			$total_pages             = (int) ceil( $expected_interval_count / $params['per_page'] );
			if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
				return array();
			}

			$intervals = array();
			$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
			$this->total_query->add_sql_clause( 'select', $selections );
			$this->total_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );

			$totals = $wpdb->get_results(
				$this->total_query->get_query_statement(),
				ARRAY_A
			); // WPCS: cache ok, DB call ok, unprepared SQL ok.

			// @todo remove these assignements when refactoring segmenter classes to use query objects.
			$totals_query          = array(
				'from_clause'       => $this->total_query->get_sql_clause( 'join' ),
				'where_time_clause' => $this->total_query->get_sql_clause( 'where_time' ),
				'where_clause'      => $this->total_query->get_sql_clause( 'where' ),
			);
			$intervals_query       = array(
				'select_clause'     => $this->get_sql_clause( 'select' ),
				'from_clause'       => $this->interval_query->get_sql_clause( 'join' ),
				'where_time_clause' => $this->interval_query->get_sql_clause( 'where_time' ),
				'where_clause'      => $this->interval_query->get_sql_clause( 'where' ),
				'order_by'          => $this->get_sql_clause( 'order_by' ),
				'limit'             => $this->get_sql_clause( 'limit' ),
			);
			$segmenter             = new Segmenter( $query_args, $this->report_columns );
			$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );

			if ( null === $totals ) {
				return new \WP_Error( 'woocommerce_analytics_products_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
			}

			$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
			$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
			$this->interval_query->add_sql_clause( 'select', ", MAX(${table_name}.date_created) AS datetime_anchor" );
			if ( '' !== $selections ) {
				$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
			}

			$intervals = $wpdb->get_results(
				$this->interval_query->get_query_statement(),
				ARRAY_A
			); // WPCS: cache ok, DB call ok, unprepared SQL ok.

			if ( null === $intervals ) {
				return new \WP_Error( 'woocommerce_analytics_products_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
			}

			$totals = (object) $this->cast_numbers( $totals[0] );

			$data = (object) array(
				'totals'    => $totals,
				'intervals' => $intervals,
				'total'     => $expected_interval_count,
				'pages'     => $total_pages,
				'page_no'   => (int) $query_args['page'],
			);

			if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
				$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
				$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
				$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
			} else {
				$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
			}
			$segmenter->add_intervals_segments( $data, $intervals_query, $table_name );
			$this->create_interval_subtotals( $data->intervals );

			$this->set_cached_data( $cache_key, $data );
		}

		return $data;
	}

	/**
	 * Normalizes order_by clause to match to SQL query.
	 *
	 * @param string $order_by Order by option requeste by user.
	 * @return string
	 */
	protected function normalize_order_by( $order_by ) {
		if ( 'date' === $order_by ) {
			return 'time_interval';
		}

		return $order_by;
	}

	/**
	 * Initialize query objects.
	 */
	protected function initialize_queries() {
		$this->clear_all_clauses();
		unset( $this->subquery );
		$this->total_query = new SqlQuery( $this->context . '_total' );
		$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );

		$this->interval_query = new SqlQuery( $this->context . '_interval' );
		$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
		$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
	}
}
Admin/API/Reports/Products/Stats/Segmenter.php000064400000024314152333011040015202 0ustar00<?php
/**
 * Class for adding segmenting support without cluttering the data stores.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Products\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Segmenter as ReportsSegmenter;
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;

/**
 * Date & time interval and numeric range handling class for Reporting API.
 */
class Segmenter extends ReportsSegmenter {

	/**
	 * Returns column => query mapping to be used for product-related product-level segmenting query
	 * (e.g. products sold, revenue from product X when segmenting by category).
	 *
	 * @param string $products_table Name of SQL table containing the product-level segmenting info.
	 *
	 * @return array Column => SELECT query mapping.
	 */
	protected function get_segment_selections_product_level( $products_table ) {
		$columns_mapping = array(
			'items_sold'       => "SUM($products_table.product_qty) as items_sold",
			'net_revenue'      => "SUM($products_table.product_net_revenue ) AS net_revenue",
			'orders_count'     => "COUNT( DISTINCT $products_table.order_id ) AS orders_count",
			'products_count'   => "COUNT( DISTINCT $products_table.product_id ) AS products_count",
			'variations_count' => "COUNT( DISTINCT $products_table.variation_id ) AS variations_count",
		);

		return $columns_mapping;
	}

	/**
	 * Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
	 *
	 * @param array  $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $segmenting_dimension_name Name of the segmenting dimension.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $totals_query Array of SQL clauses for totals query.
	 * @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
	 *
	 * @return array
	 */
	protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) {
		global $wpdb;

		$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';

		// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
		// Product-level numbers.
		$segments_products = $wpdb->get_results(
			"SELECT
						$segmenting_groupby AS $segmenting_dimension_name
						{$segmenting_selections['product_level']}
					FROM
						$table_name
						$segmenting_from
						{$totals_query['from_clause']}
					WHERE
						1=1
						{$totals_query['where_time_clause']}
						{$totals_query['where_clause']}
						$segmenting_where
					GROUP BY
						$segmenting_groupby",
			ARRAY_A
		); // WPCS: cache ok, DB call ok, unprepared SQL ok.

		$totals_segments = $this->merge_segment_totals_results( $segmenting_dimension_name, $segments_products, array() );
		return $totals_segments;
	}

	/**
	 * Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
	 *
	 * @param array  $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
	 * @param string $segmenting_from FROM part of segmenting SQL query.
	 * @param string $segmenting_where WHERE part of segmenting SQL query.
	 * @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
	 * @param string $segmenting_dimension_name Name of the segmenting dimension.
	 * @param string $table_name Name of SQL table which is the stats table for orders.
	 * @param array  $intervals_query Array of SQL clauses for intervals query.
	 * @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
	 *
	 * @return array
	 */
	protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) {
		global $wpdb;

		$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';

		// LIMIT offset, rowcount needs to be updated to a multiple of the number of segments.
		preg_match( '/LIMIT (\d+)\s?,\s?(\d+)/', $intervals_query['limit'], $limit_parts );
		$segment_count    = count( $this->get_all_segments() );
		$orig_offset      = intval( $limit_parts[1] );
		$orig_rowcount    = intval( $limit_parts[2] );
		$segmenting_limit = $wpdb->prepare( 'LIMIT %d, %d', $orig_offset * $segment_count, $orig_rowcount * $segment_count );

		// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
		// Product-level numbers.
		$segments_products = $wpdb->get_results(
			"SELECT
						{$intervals_query['select_clause']} AS time_interval,
						$segmenting_groupby AS $segmenting_dimension_name
						{$segmenting_selections['product_level']}
					FROM
						$table_name
						$segmenting_from
						{$intervals_query['from_clause']}
					WHERE
						1=1
						{$intervals_query['where_time_clause']}
						{$intervals_query['where_clause']}
						$segmenting_where
					GROUP BY
						time_interval, $segmenting_groupby
					$segmenting_limit",
			ARRAY_A
		); // WPCS: cache ok, DB call ok, unprepared SQL ok.

		$intervals_segments = $this->merge_segment_intervals_results( $segmenting_dimension_name, $segments_products, array() );
		return $intervals_segments;
	}

	/**
	 * Return array of segments formatted for REST response.
	 *
	 * @param string $type Type of segments to return--'totals' or 'intervals'.
	 * @param array  $query_params SQL query parameter array.
	 * @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
	 *
	 * @return array
	 * @throws \Automattic\WooCommerce\Admin\API\Reports\ParameterException In case of segmenting by variations, when no parent product is specified.
	 */
	protected function get_segments( $type, $query_params, $table_name ) {
		global $wpdb;
		if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
			return array();
		}

		$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
		$unique_orders_table      = 'uniq_orders';
		$segmenting_where         = '';

		// Product, variation, and category are bound to product, so here product segmenting table is required,
		// while coupon and customer are bound to order, so we don't need the extra JOIN for those.
		// This also means that segment selections need to be calculated differently.
		if ( 'product' === $this->query_args['segmentby'] ) {
			$product_level_columns     = $this->get_segment_selections_product_level( $product_segmenting_table );
			$segmenting_selections     = array(
				'product_level' => $this->prepare_selections( $product_level_columns ),
			);
			$this->report_columns      = $product_level_columns;
			$segmenting_from           = '';
			$segmenting_groupby        = $product_segmenting_table . '.product_id';
			$segmenting_dimension_name = 'product_id';

			$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
		} elseif ( 'variation' === $this->query_args['segmentby'] ) {
			if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
				throw new ParameterException( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'woocommerce' ) );
			}

			$product_level_columns     = $this->get_segment_selections_product_level( $product_segmenting_table );
			$segmenting_selections     = array(
				'product_level' => $this->prepare_selections( $product_level_columns ),
			);
			$this->report_columns      = $product_level_columns;
			$segmenting_from           = '';
			$segmenting_where          = "AND $product_segmenting_table.product_id = {$this->query_args['product_includes'][0]}";
			$segmenting_groupby        = $product_segmenting_table . '.variation_id';
			$segmenting_dimension_name = 'variation_id';

			$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
		} elseif ( 'category' === $this->query_args['segmentby'] ) {
			$product_level_columns     = $this->get_segment_selections_product_level( $product_segmenting_table );
			$segmenting_selections     = array(
				'product_level' => $this->prepare_selections( $product_level_columns ),
			);
			$this->report_columns      = $product_level_columns;
			$segmenting_from           = "
			LEFT JOIN {$wpdb->term_relationships} ON {$product_segmenting_table}.product_id = {$wpdb->term_relationships}.object_id
			JOIN {$wpdb->term_taxonomy} ON {$wpdb->term_taxonomy}.term_taxonomy_id = {$wpdb->term_relationships}.term_taxonomy_id
			LEFT JOIN {$wpdb->wc_category_lookup} ON {$wpdb->term_taxonomy}.term_id = {$wpdb->wc_category_lookup}.category_id
			";
			$segmenting_where          = " AND {$wpdb->wc_category_lookup}.category_tree_id IS NOT NULL";
			$segmenting_groupby        = "{$wpdb->wc_category_lookup}.category_tree_id";
			$segmenting_dimension_name = 'category_id';

			// Restrict our search space for category comparisons.
			if ( isset( $this->query_args['category_includes'] ) ) {
				$category_ids      = implode( ',', $this->get_all_segments() );
				$segmenting_where .= " AND {$wpdb->wc_category_lookup}.category_id IN ( $category_ids )";
			}

			$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
		}

		return $segments;
	}
}
Admin/API/Reports/Products/Stats/Query.php000064400000002426152333011040014356 0ustar00<?php
/**
 * Class for parameter-based Products Stats Report querying
 *
 * Example usage:
 * $args = array(
 *          'before'       => '2018-07-19 00:00:00',
 *          'after'        => '2018-07-05 00:00:00',
 *          'page'         => 2,
 *          'categories'   => array(15, 18),
 *          'product_ids'  => array(1,2,3)
 *         );
 * $report = new \Automattic\WooCommerce\Admin\API\Reports\Products\Stats\Query( $args );
 * $mydata = $report->get_data();
 */

namespace Automattic\WooCommerce\Admin\API\Reports\Products\Stats;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;

/**
 * API\Reports\Products\Stats\Query
 */
class Query extends ReportsQuery {

	/**
	 * Valid fields for Products report.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array();
	}

	/**
	 * Get product data based on the current query vars.
	 *
	 * @return array
	 */
	public function get_data() {
		$args = apply_filters( 'woocommerce_analytics_products_stats_query_args', $this->get_query_vars() );

		$data_store = \WC_Data_Store::load( 'report-products-stats' );
		$results    = $data_store->get_data( $args );
		return apply_filters( 'woocommerce_analytics_products_stats_select_query', $results, $args );
	}

}
Admin/API/Reports/Query.php000064400000001121152333011040011444 0ustar00<?php
/**
 * Class for parameter-based Reports querying
 */

namespace Automattic\WooCommerce\Admin\API\Reports;

defined( 'ABSPATH' ) || exit;

/**
 * Admin\API\Reports\Query
 */
abstract class Query extends \WC_Object_Query {

	/**
	 * Get report data matching the current query vars.
	 *
	 * @return array|object of WC_Product objects
	 */
	public function get_data() {
		/* translators: %s: Method name */
		return new \WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) );
	}
}
Admin/API/Reports/PerformanceIndicators/Controller.php000064400000046445152333011040016765 0ustar00<?php
/**
 * REST API Performance indicators controller
 *
 * Handles requests to the /reports/store-performance endpoint.
 */

namespace Automattic\WooCommerce\Admin\API\Reports\PerformanceIndicators;

use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports Performance indicators controller class.
 *
 * @internal
 * @extends WC_REST_Reports_Controller
 */
class Controller extends \WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/performance-indicators';

	/**
	 * Contains a list of endpoints by report slug.
	 *
	 * @var array
	 */
	protected $endpoints = array();

	/**
	 * Contains a list of active Jetpack module slugs.
	 *
	 * @var array
	 */
	protected $active_jetpack_modules = null;

	/**
	 * Contains a list of allowed stats.
	 *
	 * @var array
	 */
	protected $allowed_stats = array();

	/**
	 * Contains a list of stat labels.
	 *
	 * @var array
	 */
	protected $labels = array();

	/**
	 * Contains a list of endpoints by url.
	 *
	 * @var array
	 */
	protected $urls = array();

	/**
	 * Contains a cache of retrieved stats data, grouped by report slug.
	 *
	 * @var array
	 */
	protected $stats_data = array();

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_filter( 'woocommerce_rest_performance_indicators_data_value', array( $this, 'format_data_value' ), 10, 5 );
	}

	/**
	 * Register the routes for reports.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/allowed',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_allowed_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_allowed_item_schema' ),
			)
		);
	}

	/**
	 * Maps query arguments from the REST request.
	 *
	 * @param array $request Request array.
	 * @return array
	 */
	protected function prepare_reports_query( $request ) {
		$args           = array();
		$args['before'] = $request['before'];
		$args['after']  = $request['after'];
		$args['stats']  = $request['stats'];
		return $args;
	}

	/**
	 * Get analytics report data and endpoints.
	 */
	private function get_analytics_report_data() {
		$request  = new \WP_REST_Request( 'GET', '/wc-analytics/reports' );
		$response = rest_do_request( $request );

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		if ( 200 !== $response->get_status() ) {
			return new \WP_Error( 'woocommerce_analytics_performance_indicators_result_failed', __( 'Sorry, fetching performance indicators failed.', 'woocommerce' ) );
		}

		$endpoints = $response->get_data();

		foreach ( $endpoints as $endpoint ) {
			if ( '/stats' === substr( $endpoint['slug'], -6 ) ) {
				$request  = new \WP_REST_Request( 'OPTIONS', $endpoint['path'] );
				$response = rest_do_request( $request );

				if ( is_wp_error( $response ) ) {
					return $response;
				}

				$data = $response->get_data();

				$prefix = substr( $endpoint['slug'], 0, -6 );

				if ( empty( $data['schema']['properties']['totals']['properties'] ) ) {
					continue;
				}

				foreach ( $data['schema']['properties']['totals']['properties'] as $property_key => $schema_info ) {
					if ( empty( $schema_info['indicator'] ) || ! $schema_info['indicator'] ) {
						continue;
					}

					$stat                   = $prefix . '/' . $property_key;
					$this->allowed_stats[]  = $stat;
					$stat_label             = empty( $schema_info['title'] ) ? $schema_info['description'] : $schema_info['title'];
					$this->labels[ $stat ]  = trim( $stat_label, '.' );
					$this->formats[ $stat ] = isset( $schema_info['format'] ) ? $schema_info['format'] : 'number';
				}

				$this->endpoints[ $prefix ] = $endpoint['path'];
				$this->urls[ $prefix ]      = $endpoint['_links']['report'][0]['href'];
			}
		}
	}

	/**
	 * Get active Jetpack modules.
	 *
	 * @return array List of active Jetpack module slugs.
	 */
	private function get_active_jetpack_modules() {
		if ( is_null( $this->active_jetpack_modules ) ) {
			if ( class_exists( '\Jetpack' ) && method_exists( '\Jetpack', 'get_active_modules' ) ) {
				$active_modules               = \Jetpack::get_active_modules();
				$this->active_jetpack_modules = is_array( $active_modules ) ? $active_modules : array();
			} else {
				$this->active_jetpack_modules = array();
			}
		}

		return $this->active_jetpack_modules;
	}

	/**
	 * Set active Jetpack modules.
	 *
	 * @internal
	 * @param array $modules List of active Jetpack module slugs.
	 */
	public function set_active_jetpack_modules( $modules ) {
		$this->active_jetpack_modules = $modules;
	}

	/**
	 * Get active Jetpack modules and endpoints.
	 */
	private function get_jetpack_modules_data() {
		$active_modules = $this->get_active_jetpack_modules();

		if ( empty( $active_modules ) ) {
			return;
		}

		$items = apply_filters(
			'woocommerce_rest_performance_indicators_jetpack_items',
			array(
				'stats/visitors' => array(
					'label'      => __( 'Visitors', 'woocommerce' ),
					'permission' => 'view_stats',
					'format'     => 'number',
					'module'     => 'stats',
				),
				'stats/views'    => array(
					'label'      => __( 'Views', 'woocommerce' ),
					'permission' => 'view_stats',
					'format'     => 'number',
					'module'     => 'stats',
				),
			)
		);

		foreach ( $items as $item_key => $item ) {
			if ( ! in_array( $item['module'], $active_modules, true ) ) {
				return;
			}

			if ( $item['permission'] && ! current_user_can( $item['permission'] ) ) {
				return;
			}

			$stat                         = 'jetpack/' . $item_key;
			$endpoint                     = 'jetpack/' . $item['module'];
			$this->allowed_stats[]        = $stat;
			$this->labels[ $stat ]        = $item['label'];
			$this->endpoints[ $endpoint ] = '/jetpack/v4/module/' . $item['module'] . '/data';
			$this->formats[ $stat ]       = $item['format'];
		}

		$this->urls['jetpack/stats'] = '/jetpack';
	}

	/**
	 * Get information such as allowed stats, stat labels, and endpoint data from stats reports.
	 *
	 * @return WP_Error|True
	 */
	private function get_indicator_data() {
		// Data already retrieved.
		if ( ! empty( $this->endpoints ) && ! empty( $this->labels ) && ! empty( $this->allowed_stats ) ) {
			return true;
		}

		$this->get_analytics_report_data();
		$this->get_jetpack_modules_data();

		return true;
	}

	/**
	 * Returns a list of allowed performance indicators.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_allowed_items( $request ) {
		$indicator_data = $this->get_indicator_data();
		if ( is_wp_error( $indicator_data ) ) {
			return $indicator_data;
		}

		$data = array();
		foreach ( $this->allowed_stats as $stat ) {
			$pieces = $this->get_stats_parts( $stat );
			$report = $pieces[0];
			$chart  = $pieces[1];
			$data[] = (object) array(
				'stat'  => $stat,
				'chart' => $chart,
				'label' => $this->labels[ $stat ],
			);
		}

		usort( $data, array( $this, 'sort' ) );

		$objects = array();
		foreach ( $data as $item ) {
			$prepared  = $this->prepare_item_for_response( $item, $request );
			$objects[] = $this->prepare_response_for_collection( $prepared );
		}

		$response = rest_ensure_response( $objects );
		$response->header( 'X-WP-Total', count( $data ) );
		$response->header( 'X-WP-TotalPages', 1 );

		$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );

		return $response;
	}

	/**
	 * Sorts the list of stats. Sorted by custom arrangement.
	 *
	 * @internal
	 * @see https://github.com/woocommerce/woocommerce-admin/issues/1282
	 * @param object $a First item.
	 * @param object $b Second item.
	 * @return order
	 */
	public function sort( $a, $b ) {
		/**
		 * Custom ordering for store performance indicators.
		 *
		 * @see https://github.com/woocommerce/woocommerce-admin/issues/1282
		 * @param array $indicators A list of ordered indicators.
		 */
		$stat_order = apply_filters(
			'woocommerce_rest_report_sort_performance_indicators',
			array(
				'revenue/total_sales',
				'revenue/net_revenue',
				'orders/orders_count',
				'orders/avg_order_value',
				'products/items_sold',
				'revenue/refunds',
				'coupons/orders_count',
				'coupons/amount',
				'taxes/total_tax',
				'taxes/order_tax',
				'taxes/shipping_tax',
				'revenue/shipping',
				'downloads/download_count',
			)
		);

		$a = array_search( $a->stat, $stat_order, true );
		$b = array_search( $b->stat, $stat_order, true );

		if ( false === $a && false === $b ) {
			return 0;
		} elseif ( false === $a ) {
			return 1;
		} elseif ( false === $b ) {
			return -1;
		} else {
			return $a - $b;
		}
	}

	/**
	 * Get report stats data, avoiding duplicate requests for stats that use the same endpoint.
	 *
	 * @param string $report Report slug to request data for.
	 * @param array  $query_args Report query args.
	 * @return WP_REST_Response|WP_Error Report stats data.
	 */
	private function get_stats_data( $report, $query_args ) {
		// Return from cache if we've already requested these report stats.
		if ( isset( $this->stats_data[ $report ] ) ) {
			return $this->stats_data[ $report ];
		}

		// Request the report stats.
		$request_url = $this->endpoints[ $report ];
		$request     = new \WP_REST_Request( 'GET', $request_url );
		$request->set_param( 'before', $query_args['before'] );
		$request->set_param( 'after', $query_args['after'] );

		$response = rest_do_request( $request );

		// Cache the response.
		$this->stats_data[ $report ] = $response;

		return $response;
	}

	/**
	 * Get all reports.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$indicator_data = $this->get_indicator_data();
		if ( is_wp_error( $indicator_data ) ) {
			return $indicator_data;
		}

		$query_args = $this->prepare_reports_query( $request );
		if ( empty( $query_args['stats'] ) ) {
			return new \WP_Error( 'woocommerce_analytics_performance_indicators_empty_query', __( 'A list of stats to query must be provided.', 'woocommerce' ), 400 );
		}

		$stats = array();
		foreach ( $query_args['stats'] as $stat ) {
			$is_error = false;

			$pieces = $this->get_stats_parts( $stat );
			$report = $pieces[0];
			$chart  = $pieces[1];

			if ( ! in_array( $stat, $this->allowed_stats, true ) ) {
				continue;
			}

			$response = $this->get_stats_data( $report, $query_args );

			if ( is_wp_error( $response ) ) {
				return $response;
			}

			$data   = $response->get_data();
			$format = $this->formats[ $stat ];
			$label  = $this->labels[ $stat ];

			if ( 200 !== $response->get_status() ) {
				$stats[] = (object) array(
					'stat'   => $stat,
					'chart'  => $chart,
					'label'  => $label,
					'format' => $format,
					'value'  => null,
				);
				continue;
			}

			$stats[] = (object) array(
				'stat'   => $stat,
				'chart'  => $chart,
				'label'  => $label,
				'format' => $format,
				'value'  => apply_filters( 'woocommerce_rest_performance_indicators_data_value', $data, $stat, $report, $chart, $query_args ),
			);
		}

		usort( $stats, array( $this, 'sort' ) );

		$objects = array();
		foreach ( $stats as $stat ) {
			$data      = $this->prepare_item_for_response( $stat, $request );
			$objects[] = $this->prepare_response_for_collection( $data );
		}

		$response = rest_ensure_response( $objects );
		$response->header( 'X-WP-Total', count( $stats ) );
		$response->header( 'X-WP-TotalPages', 1 );

		$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );

		return $response;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param stdClass        $stat_data    Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $stat_data, $request ) {
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $stat_data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $data ) );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_performance_indicators', $response, $stat_data, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param \Automattic\WooCommerce\Admin\API\Reports\Query $object Object data.
	 * @return array
	 */
	protected function prepare_links( $object ) {
		$pieces   = $this->get_stats_parts( $object->stat );
		$endpoint = $pieces[0];
		$stat     = $pieces[1];
		$url      = isset( $this->urls[ $endpoint ] ) ? $this->urls[ $endpoint ] : '';

		$links = array(
			'api'    => array(
				'href' => rest_url( $this->endpoints[ $endpoint ] ),
			),
			'report' => array(
				'href' => $url,
			),
		);

		return $links;
	}

	/**
	 * Returns the endpoint part of a stat request (prefix) and the actual stat total we want.
	 * To allow extensions to namespace (example: fue/emails/sent), we break on the last forward slash.
	 *
	 * @param string $full_stat A stat request string like orders/avg_order_value or fue/emails/sent.
	 * @return array Containing the prefix (endpoint) and suffix (stat).
	 */
	private function get_stats_parts( $full_stat ) {
		$endpoint = substr( $full_stat, 0, strrpos( $full_stat, '/' ) );
		$stat     = substr( $full_stat, ( strrpos( $full_stat, '/' ) + 1 ) );
		return array(
			$endpoint,
			$stat,
		);
	}

	/**
	 * Format the data returned from the API for given stats.
	 *
	 * @param array  $data Data from external endpoint.
	 * @param string $stat Name of the stat.
	 * @param string $report Name of the report.
	 * @param string $chart Name of the chart.
	 * @param array  $query_args Query args.
	 * @return mixed
	 */
	public function format_data_value( $data, $stat, $report, $chart, $query_args ) {
		if ( 'jetpack/stats' === $report ) {
			// Get the index of the field to tally.
			$index = array_search( $chart, $data['general']->visits->fields, true );
			if ( ! $index ) {
				return null;
			}

			// Loop over provided data and filter by the queried date.
			// Note that this is currently limited to 30 days via the Jetpack API
			// but the WordPress.com endpoint allows up to 90 days.
			$total  = 0;
			$before = gmdate( 'Y-m-d', strtotime( isset( $query_args['before'] ) ? $query_args['before'] : TimeInterval::default_before() ) );
			$after  = gmdate( 'Y-m-d', strtotime( isset( $query_args['after'] ) ? $query_args['after'] : TimeInterval::default_after() ) );
			foreach ( $data['general']->visits->data as $datum ) {
				if ( $datum[0] >= $after && $datum[0] <= $before ) {
					$total += $datum[ $index ];
				}
			}
			return $total;
		}

		if ( isset( $data['totals'] ) && isset( $data['totals'][ $chart ] ) ) {
			return $data['totals'][ $chart ];
		}

		return null;
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$indicator_data = $this->get_indicator_data();
		if ( is_wp_error( $indicator_data ) ) {
			$allowed_stats = array();
		} else {
			$allowed_stats = $this->allowed_stats;
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_performance_indicator',
			'type'       => 'object',
			'properties' => array(
				'stat'   => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'enum'        => $allowed_stats,
				),
				'chart'  => array(
					'description' => __( 'The specific chart this stat referrers to.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'label'  => array(
					'description' => __( 'Human readable label for the stat.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'format' => array(
					'description' => __( 'Format of the stat.', 'woocommerce' ),
					'type'        => 'number',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'enum'        => array( 'number', 'currency' ),
				),
				'value'  => array(
					'description' => __( 'Value of the stat. Returns null if the stat does not exist or cannot be loaded.', 'woocommerce' ),
					'type'        => 'number',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get schema for the list of allowed performance indicators.
	 *
	 * @return array $schema
	 */
	public function get_public_allowed_item_schema() {
		$schema = $this->get_public_item_schema();
		unset( $schema['properties']['value'] );
		unset( $schema['properties']['format'] );
		return $schema;
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$indicator_data = $this->get_indicator_data();
		if ( is_wp_error( $indicator_data ) ) {
			$allowed_stats = __( 'There was an issue loading the report endpoints', 'woocommerce' );
		} else {
			$allowed_stats = implode( ', ', $this->allowed_stats );
		}

		$params            = array();
		$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
		$params['stats']   = array(
			'description'       => sprintf(
				/* translators: Allowed values is a list of stat endpoints. */
				__( 'Limit response to specific report stats. Allowed values: %s.', 'woocommerce' ),
				$allowed_stats
			),
			'type'              => 'array',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'type' => 'string',
				'enum' => $this->allowed_stats,
			),
			'default'           => $this->allowed_stats,
		);
		$params['after']   = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']  = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		return $params;
	}
}
Admin/API/Themes.php000064400000014141152333011040010134 0ustar00<?php
/**
 * REST API Themes Controller
 *
 * Handles requests to /themes
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Overrides\ThemeUpgrader;
use Automattic\WooCommerce\Admin\Overrides\ThemeUpgraderSkin;

/**
 * Themes controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class Themes extends \WC_REST_Data_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'themes';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'upload_theme' ),
					'permission_callback' => array( $this, 'upload_theme_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to edit upload plugins/themes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function upload_theme_permissions_check( $request ) {
		if ( ! current_user_can( 'upload_themes' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you are not allowed to install themes on this site.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Upload and install a theme.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function upload_theme( $request ) {
		if (
			! isset( $_FILES['pluginzip'] ) || ! isset( $_FILES['pluginzip']['tmp_name'] ) || ! is_uploaded_file( $_FILES['pluginzip']['tmp_name'] ) || ! is_file( $_FILES['pluginzip']['tmp_name'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,  WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			return new \WP_Error( 'woocommerce_rest_invalid_file', __( 'Specified file failed upload test.', 'woocommerce' ) );
		}

		include_once ABSPATH . 'wp-admin/includes/file.php';
		include_once ABSPATH . '/wp-admin/includes/admin.php';
		include_once ABSPATH . '/wp-admin/includes/theme-install.php';
		include_once ABSPATH . '/wp-admin/includes/theme.php';
		include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
		include_once ABSPATH . '/wp-admin/includes/class-theme-upgrader.php';

		$_GET['package'] = true;
		$file_upload     = new \File_Upload_Upgrader( 'pluginzip', 'package' );
		$upgrader        = new ThemeUpgrader( new ThemeUpgraderSkin() );
		$install         = $upgrader->install( $file_upload->package );

		if ( $install || is_wp_error( $install ) ) {
			$file_upload->cleanup();
		}

		if ( ! is_wp_error( $install ) && isset( $install['destination_name'] ) ) {
			$theme  = $install['destination_name'];
			$result = array(
				'status'  => 'success',
				'message' => $upgrader->strings['process_success'],
				'theme'   => $theme,
			);

			/**
			 * Fires when a theme is successfully installed.
			 *
			 * @param string $theme The theme name.
			 */
			do_action( 'woocommerce_theme_installed', $theme );
		} else {
			if ( is_wp_error( $install ) && $install->get_error_code() ) {
				$error_message = isset( $upgrader->strings[ $install->get_error_code() ] ) ? $upgrader->strings[ $install->get_error_code() ] : $install->get_error_data();
			} else {
				$error_message = $upgrader->strings['process_failed'];
			}

			$result = array(
				'status'  => 'error',
				'message' => $error_message,
			);
		}

		$response = $this->prepare_item_for_response( $result, $request );
		$data     = $this->prepare_response_for_collection( $response );

		return rest_ensure_response( $data );
	}


	/**
	 * Prepare the data object for response.
	 *
	 * @param object          $item Data object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data     = $this->add_additional_fields_to_object( $item, $request );
		$data     = $this->filter_response_by_context( $data, 'view' );
		$response = rest_ensure_response( $data );

		/**
		 * Filter the list returned from the API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param array            $item     The original item.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_themes', $response, $item, $request );
	}


	/**
	 * Get the schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'upload_theme',
			'type'       => 'object',
			'properties' => array(
				'status'  => array(
					'description' => __( 'Theme installation status.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'message' => array(
					'description' => __( 'Theme installation message.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'theme'   => array(
					'description' => __( 'Uploaded theme.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params['context']   = $this->get_context_param( array( 'default' => 'view' ) );
		$params['pluginzip'] = array(
			'description'       => __( 'A zip file of the theme to be uploaded.', 'woocommerce' ),
			'type'              => 'file',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return apply_filters( 'woocommerce_rest_themes_collection_params', $params );
	}
}
Admin/API/Leaderboards.php000064400000043323152333011040011302 0ustar00<?php
/**
 * REST API Leaderboards Controller
 *
 * Handles requests to /leaderboards
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Categories\DataStore as CategoriesDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsDataStore;

/**
 * Leaderboards controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class Leaderboards extends \WC_REST_Data_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'leaderboards';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/allowed',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_allowed_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_allowed_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<leaderboard>\w+)',
			array(
				'args' => array(
					'leaderboard' => array(
						'type' => 'string',
						'enum' => array( 'customers', 'coupons', 'categories', 'products' ),
					),
				),
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get the data for the coupons leaderboard.
	 *
	 * @param int    $per_page Number of rows.
	 * @param string $after Items after date.
	 * @param string $before Items before date.
	 * @param string $persisted_query URL query string.
	 */
	protected function get_coupons_leaderboard( $per_page, $after, $before, $persisted_query ) {
		$coupons_data_store = new CouponsDataStore();
		$coupons_data       = $per_page > 0 ? $coupons_data_store->get_data(
			apply_filters(
				'woocommerce_analytics_coupons_query_args',
				array(
					'orderby'       => 'orders_count',
					'order'         => 'desc',
					'after'         => $after,
					'before'        => $before,
					'per_page'      => $per_page,
					'extended_info' => true,
				)
			)
		)->data : array();

		$rows = array();
		foreach ( $coupons_data as $coupon ) {
			$url_query   = wp_parse_args(
				array(
					'filter'  => 'single_coupon',
					'coupons' => $coupon['coupon_id'],
				),
				$persisted_query
			);
			$coupon_url  = wc_admin_url( '/analytics/coupons', $url_query );
			$coupon_code = isset( $coupon['extended_info'] ) && isset( $coupon['extended_info']['code'] ) ? $coupon['extended_info']['code'] : '';
			$rows[]      = array(
				array(
					'display' => "<a href='{$coupon_url}'>{$coupon_code}</a>",
					'value'   => $coupon_code,
				),
				array(
					'display' => wc_admin_number_format( $coupon['orders_count'] ),
					'value'   => $coupon['orders_count'],
				),
				array(
					'display' => wc_price( $coupon['amount'] ),
					'value'   => $coupon['amount'],
				),
			);
		}

		return array(
			'id'      => 'coupons',
			'label'   => __( 'Top Coupons - Number of Orders', 'woocommerce' ),
			'headers' => array(
				array(
					'label' => __( 'Coupon code', 'woocommerce' ),
				),
				array(
					'label' => __( 'Orders', 'woocommerce' ),
				),
				array(
					'label' => __( 'Amount discounted', 'woocommerce' ),
				),
			),
			'rows'    => $rows,
		);
	}

	/**
	 * Get the data for the categories leaderboard.
	 *
	 * @param int    $per_page Number of rows.
	 * @param string $after Items after date.
	 * @param string $before Items before date.
	 * @param string $persisted_query URL query string.
	 */
	protected function get_categories_leaderboard( $per_page, $after, $before, $persisted_query ) {
		$categories_data_store = new CategoriesDataStore();
		$categories_data       = $per_page > 0 ? $categories_data_store->get_data(
			apply_filters(
				'woocommerce_analytics_categories_query_args',
				array(
					'orderby'       => 'items_sold',
					'order'         => 'desc',
					'after'         => $after,
					'before'        => $before,
					'per_page'      => $per_page,
					'extended_info' => true,
				)
			)
		)->data : array();

		$rows = array();
		foreach ( $categories_data as $category ) {
			$url_query     = wp_parse_args(
				array(
					'filter'     => 'single_category',
					'categories' => $category['category_id'],
				),
				$persisted_query
			);
			$category_url  = wc_admin_url( '/analytics/categories', $url_query );
			$category_name = isset( $category['extended_info'] ) && isset( $category['extended_info']['name'] ) ? $category['extended_info']['name'] : '';
			$rows[]        = array(
				array(
					'display' => "<a href='{$category_url}'>{$category_name}</a>",
					'value'   => $category_name,
				),
				array(
					'display' => wc_admin_number_format( $category['items_sold'] ),
					'value'   => $category['items_sold'],
				),
				array(
					'display' => wc_price( $category['net_revenue'] ),
					'value'   => $category['net_revenue'],
				),
			);
		}

		return array(
			'id'      => 'categories',
			'label'   => __( 'Top categories - Items sold', 'woocommerce' ),
			'headers' => array(
				array(
					'label' => __( 'Category', 'woocommerce' ),
				),
				array(
					'label' => __( 'Items sold', 'woocommerce' ),
				),
				array(
					'label' => __( 'Net sales', 'woocommerce' ),
				),
			),
			'rows'    => $rows,
		);
	}

	/**
	 * Get the data for the customers leaderboard.
	 *
	 * @param int    $per_page Number of rows.
	 * @param string $after Items after date.
	 * @param string $before Items before date.
	 * @param string $persisted_query URL query string.
	 */
	protected function get_customers_leaderboard( $per_page, $after, $before, $persisted_query ) {
		$customers_data_store = new CustomersDataStore();
		$customers_data       = $per_page > 0 ? $customers_data_store->get_data(
			apply_filters(
				'woocommerce_analytics_customers_query_args',
				array(
					'orderby'      => 'total_spend',
					'order'        => 'desc',
					'order_after'  => $after,
					'order_before' => $before,
					'per_page'     => $per_page,
				)
			)
		)->data : array();

		$rows = array();
		foreach ( $customers_data as $customer ) {
			$url_query    = wp_parse_args(
				array(
					'filter'    => 'single_customer',
					'customers' => $customer['id'],
				),
				$persisted_query
			);
			$customer_url = wc_admin_url( '/analytics/customers', $url_query );
			$rows[]       = array(
				array(
					'display' => "<a href='{$customer_url}'>{$customer['name']}</a>",
					'value'   => $customer['name'],
				),
				array(
					'display' => wc_admin_number_format( $customer['orders_count'] ),
					'value'   => $customer['orders_count'],
				),
				array(
					'display' => wc_price( $customer['total_spend'] ),
					'value'   => $customer['total_spend'],
				),
			);
		}

		return array(
			'id'      => 'customers',
			'label'   => __( 'Top Customers - Total Spend', 'woocommerce' ),
			'headers' => array(
				array(
					'label' => __( 'Customer Name', 'woocommerce' ),
				),
				array(
					'label' => __( 'Orders', 'woocommerce' ),
				),
				array(
					'label' => __( 'Total Spend', 'woocommerce' ),
				),
			),
			'rows'    => $rows,
		);
	}

	/**
	 * Get the data for the products leaderboard.
	 *
	 * @param int    $per_page Number of rows.
	 * @param string $after Items after date.
	 * @param string $before Items before date.
	 * @param string $persisted_query URL query string.
	 */
	protected function get_products_leaderboard( $per_page, $after, $before, $persisted_query ) {
		$products_data_store = new ProductsDataStore();
		$products_data       = $per_page > 0 ? $products_data_store->get_data(
			apply_filters(
				'woocommerce_analytics_products_query_args',
				array(
					'orderby'       => 'items_sold',
					'order'         => 'desc',
					'after'         => $after,
					'before'        => $before,
					'per_page'      => $per_page,
					'extended_info' => true,
				)
			)
		)->data : array();

		$rows = array();
		foreach ( $products_data as $product ) {
			$url_query    = wp_parse_args(
				array(
					'filter'   => 'single_product',
					'products' => $product['product_id'],
				),
				$persisted_query
			);
			$product_url  = wc_admin_url( '/analytics/products', $url_query );
			$product_name = isset( $product['extended_info'] ) && isset( $product['extended_info']['name'] ) ? $product['extended_info']['name'] : '';
			$rows[]       = array(
				array(
					'display' => "<a href='{$product_url}'>{$product_name}</a>",
					'value'   => $product_name,
				),
				array(
					'display' => wc_admin_number_format( $product['items_sold'] ),
					'value'   => $product['items_sold'],
				),
				array(
					'display' => wc_price( $product['net_revenue'] ),
					'value'   => $product['net_revenue'],
				),
			);
		}

		return array(
			'id'      => 'products',
			'label'   => __( 'Top products - Items sold', 'woocommerce' ),
			'headers' => array(
				array(
					'label' => __( 'Product', 'woocommerce' ),
				),
				array(
					'label' => __( 'Items sold', 'woocommerce' ),
				),
				array(
					'label' => __( 'Net sales', 'woocommerce' ),
				),
			),
			'rows'    => $rows,
		);
	}

	/**
	 * Get an array of all leaderboards.
	 *
	 * @param int    $per_page Number of rows.
	 * @param string $after Items after date.
	 * @param string $before Items before date.
	 * @param string $persisted_query URL query string.
	 * @return array
	 */
	public function get_leaderboards( $per_page, $after, $before, $persisted_query ) {
		$leaderboards = array(
			$this->get_customers_leaderboard( $per_page, $after, $before, $persisted_query ),
			$this->get_coupons_leaderboard( $per_page, $after, $before, $persisted_query ),
			$this->get_categories_leaderboard( $per_page, $after, $before, $persisted_query ),
			$this->get_products_leaderboard( $per_page, $after, $before, $persisted_query ),
		);

		return apply_filters( 'woocommerce_leaderboards', $leaderboards, $per_page, $after, $before, $persisted_query );
	}

	/**
	 * Return all leaderboards.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$persisted_query = json_decode( $request['persisted_query'], true );

		switch ( $request['leaderboard'] ) {
			case 'customers':
				$leaderboards = array( $this->get_customers_leaderboard( $request['per_page'], $request['after'], $request['before'], $persisted_query ) );
				break;
			case 'coupons':
				$leaderboards = array( $this->get_coupons_leaderboard( $request['per_page'], $request['after'], $request['before'], $persisted_query ) );
				break;
			case 'categories':
				$leaderboards = array( $this->get_categories_leaderboard( $request['per_page'], $request['after'], $request['before'], $persisted_query ) );
				break;
			case 'products':
				$leaderboards = array( $this->get_products_leaderboard( $request['per_page'], $request['after'], $request['before'], $persisted_query ) );
				break;
			default:
				$leaderboards = $this->get_leaderboards( $request['per_page'], $request['after'], $request['before'], $persisted_query );
				break;
		}

		$data = array();
		if ( ! empty( $leaderboards ) ) {
			foreach ( $leaderboards as $leaderboard ) {
				$response = $this->prepare_item_for_response( $leaderboard, $request );
				$data[]   = $this->prepare_response_for_collection( $response );
			}
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Returns a list of allowed leaderboards.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return array|WP_Error
	 */
	public function get_allowed_items( $request ) {
		$leaderboards = $this->get_leaderboards( 0, null, null, null );

		$data = array();
		foreach ( $leaderboards as $leaderboard ) {
			$data[] = (object) array(
				'id'      => $leaderboard['id'],
				'label'   => $leaderboard['label'],
				'headers' => $leaderboard['headers'],
			);
		}

		$objects = array();
		foreach ( $data as $item ) {
			$prepared  = $this->prepare_item_for_response( $item, $request );
			$objects[] = $this->prepare_response_for_collection( $prepared );
		}

		$response = rest_ensure_response( $objects );
		$response->header( 'X-WP-Total', count( $data ) );
		$response->header( 'X-WP-TotalPages', 1 );

		$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );

		return $response;
	}

	/**
	 * Prepare the data object for response.
	 *
	 * @param object          $item Data object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data     = $this->add_additional_fields_to_object( $item, $request );
		$data     = $this->filter_response_by_context( $data, 'view' );
		$response = rest_ensure_response( $data );

		/**
		 * Filter the list returned from the API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param array            $item     The original item.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_leaderboard', $response, $item, $request );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                    = array();
		$params['page']            = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']        = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 5,
			'minimum'           => 1,
			'maximum'           => 20,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']           = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']          = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['persisted_query'] = array(
			'description'       => __( 'URL query to persist across links.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		return $params;
	}

	/**
	 * Get the schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'leaderboard',
			'type'       => 'object',
			'properties' => array(
				'id'      => array(
					'type'        => 'string',
					'description' => __( 'Leaderboard ID.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'label'   => array(
					'type'        => 'string',
					'description' => __( 'Displayed title for the leaderboard.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'headers' => array(
					'type'        => 'array',
					'description' => __( 'Table headers.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'array',
						'properties' => array(
							'label' => array(
								'description' => __( 'Table column header.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'rows'    => array(
					'type'        => 'array',
					'description' => __( 'Table rows.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'array',
						'properties' => array(
							'display' => array(
								'description' => __( 'Table cell display.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'value'   => array(
								'description' => __( 'Table cell value.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get schema for the list of allowed leaderboards.
	 *
	 * @return array $schema
	 */
	public function get_public_allowed_item_schema() {
		$schema = $this->get_public_item_schema();
		unset( $schema['properties']['rows'] );
		return $schema;
	}
}
Admin/API/Data.php000064400000001653152333011040007564 0ustar00<?php
/**
 * REST API Data Controller
 *
 * Handles requests to /data
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * Data controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class Data extends \WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Return the list of data resources.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$response         = parent::get_items( $request );
		$response->data[] = $this->prepare_response_for_collection(
			$this->prepare_item_for_response(
				(object) array(
					'slug'        => 'download-ips',
					'description' => __( 'An endpoint used for searching download logs for a specific IP address.', 'woocommerce' ),
				),
				$request
			)
		);
		return $response;
	}
}
Admin/API/OnboardingTasks.php000064400000100613152333011040011777 0ustar00<?php
/**
 * REST API Onboarding Tasks Controller
 *
 * Handles requests to complete various onboarding tasks.
 */

namespace Automattic\WooCommerce\Admin\API;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingIndustries;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedExtendedTask;

defined( 'ABSPATH' ) || exit;

/**
 * Onboarding Tasks Controller.
 *
 * @internal
 * @extends WC_REST_Data_Controller
 */
class OnboardingTasks extends \WC_REST_Data_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-admin';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'onboarding/tasks';

	/**
	 * Duration to milisecond mapping.
	 *
	 * @var string
	 */
	protected $duration_to_ms = array(
		'day'  => DAY_IN_SECONDS * 1000,
		'hour' => HOUR_IN_SECONDS * 1000,
		'week' => WEEK_IN_SECONDS * 1000,
	);

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/import_sample_products',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'import_sample_products' ),
					'permission_callback' => array( $this, 'create_products_permission_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/create_homepage',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_homepage' ),
					'permission_callback' => array( $this, 'create_pages_permission_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/create_product_from_template',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_product_from_template' ),
					'permission_callback' => array( $this, 'create_products_permission_check' ),
					'args'                => array_merge(
						$this->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ),
						array(
							'template_name' => array(
								'required'    => true,
								'type'        => 'string',
								'description' => __( 'Product template name.', 'woocommerce' ),
							),
						)
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_tasks' ),
					'permission_callback' => array( $this, 'get_tasks_permission_check' ),
					'args'                => array(
						'ids' => array(
							'description'       => __( 'Optional parameter to get only specific task lists by id.', 'woocommerce' ),
							'type'              => 'array',
							'sanitize_callback' => 'wp_parse_slug_list',
							'validate_callback' => 'rest_validate_request_arg',
							'items'             => array(
								'enum' => TaskLists::get_list_ids(),
								'type' => 'string',
							),
						),
					),
				),
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'get_tasks' ),
					'permission_callback' => array( $this, 'get_tasks_permission_check' ),
					'args'                => $this->get_task_list_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[a-z0-9_\-]+)/hide',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'hide_task_list' ),
					'permission_callback' => array( $this, 'hide_task_list_permission_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[a-z0-9_\-]+)/unhide',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'unhide_task_list' ),
					'permission_callback' => array( $this, 'hide_task_list_permission_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[a-z0-9_\-]+)/dismiss',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'dismiss_task' ),
					'permission_callback' => array( $this, 'get_tasks_permission_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[a-z0-9_\-]+)/undo_dismiss',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'undo_dismiss_task' ),
					'permission_callback' => array( $this, 'get_tasks_permission_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[a-z0-9_-]+)/snooze',
			array(
				'args'   => array(
					'duration'     => array(
						'description'       => __( 'Time period to snooze the task.', 'woocommerce' ),
						'type'              => 'string',
						'validate_callback' => function( $param, $request, $key ) {
							return in_array( $param, array_keys( $this->duration_to_ms ), true );
						},
					),
					'task_list_id' => array(
						'description' => __( 'Optional parameter to query specific task list.', 'woocommerce' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'snooze_task' ),
					'permission_callback' => array( $this, 'snooze_task_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[a-z0-9_\-]+)/action',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'action_task' ),
					'permission_callback' => array( $this, 'get_tasks_permission_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[a-z0-9_\-]+)/undo_snooze',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'undo_snooze_task' ),
					'permission_callback' => array( $this, 'snooze_task_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check if a given request has access to create a product.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function create_products_permission_check( $request ) {
		if ( ! wc_rest_check_post_permissions( 'product', 'create' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to create a product.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function create_pages_permission_check( $request ) {
		if ( ! wc_rest_check_post_permissions( 'page', 'create' ) || ! current_user_can( 'manage_options' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create new pages.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to manage woocommerce.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_tasks_permission_check( $request ) {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to retrieve onboarding tasks.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has permission to hide task lists.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function hide_task_list_permission_check( $request ) {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you are not allowed to hide task lists.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to manage woocommerce.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function snooze_task_permissions_check( $request ) {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			return new \WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to snooze onboarding tasks.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Import sample products from given CSV path.
	 *
	 * @param  string $csv_file CSV file path.
	 * @return WP_Error|WP_REST_Response
	 */
	public static function import_sample_products_from_csv( $csv_file ) {
		include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php';

		if ( file_exists( $csv_file ) && class_exists( 'WC_Product_CSV_Importer' ) ) {
			// Override locale so we can return mappings from WooCommerce in English language stores.
			add_filter( 'locale', '__return_false', 9999 );
			$importer_class = apply_filters( 'woocommerce_product_csv_importer_class', 'WC_Product_CSV_Importer' );
			$args           = array(
				'parse'   => true,
				'mapping' => self::get_header_mappings( $csv_file ),
			);
			$args           = apply_filters( 'woocommerce_product_csv_importer_args', $args, $importer_class );

			$importer = new $importer_class( $csv_file, $args );
			$import   = $importer->import();
			return $import;
		} else {
			return new \WP_Error( 'woocommerce_rest_import_error', __( 'Sorry, the sample products data file was not found.', 'woocommerce' ) );
		}
	}

	/**
	 * Import sample products from WooCommerce sample CSV.
	 *
	 * @internal
	 * @return WP_Error|WP_REST_Response
	 */
	public static function import_sample_products() {
		if (
			( Features::is_enabled( 'experimental-import-products-task' ) || Features::is_enabled( 'experimental-products-task' ) )
			&& static::is_experiment_product_task()
		) {

			$sample_csv_file = Features::is_enabled( 'experimental-fashion-sample-products' ) ? WC_ABSPATH . 'sample-data/experimental_fashion_sample_9_products.csv' :
			WC_ABSPATH . 'sample-data/experimental_sample_9_products.csv';
		} else {
			$sample_csv_file = WC_ABSPATH . 'sample-data/sample_products.csv';
		}

		$import = self::import_sample_products_from_csv( $sample_csv_file );
		return rest_ensure_response( $import );
	}

	/**
	 * Check if product task experiment is treatment.
	 *
	 * @return bool
	 */
	public static function is_experiment_product_task() {
		$anon_id        = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : '';
		$allow_tracking = get_option( 'woocommerce_allow_tracking' ) === 'yes';
		$abtest         = new \WooCommerce\Admin\Experimental_Abtest(
			$anon_id,
			'woocommerce',
			$allow_tracking
		);
		return $abtest->get_variation( 'woocommerce_products_task_layout_stacked_v3' ) === 'treatment' ||
			$abtest->get_variation( 'woocommerce_products_task_layout_card_v3' ) === 'treatment';
	}

	/**
	 * Creates a product from a template name passed in through the template_name param.
	 *
	 * @internal
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public static function create_product_from_template( $request ) {
		$template_name = $request->get_param( 'template_name' );
		$template_path = __DIR__ . '/Templates/' . $template_name . '_product.csv';
		$template_path = apply_filters( 'woocommerce_product_template_csv_file_path', $template_path, $template_name );

		$import = self::import_sample_products_from_csv( $template_path );

		if ( is_wp_error( $import ) || 0 === count( $import['imported'] ) ) {
			return new \WP_Error(
				'woocommerce_rest_product_creation_error',
				/* translators: %s is template name */
				__( 'Sorry, creating the product with template failed.', 'woocommerce' ),
				array( 'status' => 500 )
			);
		}
		$product = wc_get_product( $import['imported'][0] );
		$product->set_status( 'auto-draft' );
		$product->save();

		return rest_ensure_response(
			array(
				'id' => $product->get_id(),
			)
		);
	}


	/**
	 * Get header mappings from CSV columns.
	 *
	 * @internal
	 * @param string $file File path.
	 * @return array Mapped headers.
	 */
	public static function get_header_mappings( $file ) {
		include_once WC_ABSPATH . 'includes/admin/importers/mappings/mappings.php';

		$importer_class  = apply_filters( 'woocommerce_product_csv_importer_class', 'WC_Product_CSV_Importer' );
		$importer        = new $importer_class( $file, array() );
		$raw_headers     = $importer->get_raw_keys();
		$default_columns = wc_importer_default_english_mappings( array() );
		$special_columns = wc_importer_default_special_english_mappings( array() );

		$headers = array();
		foreach ( $raw_headers as $key => $field ) {
			$index             = $field;
			$headers[ $index ] = $field;

			if ( isset( $default_columns[ $field ] ) ) {
				$headers[ $index ] = $default_columns[ $field ];
			} else {
				foreach ( $special_columns as $regex => $special_key ) {
					if ( preg_match( self::sanitize_special_column_name_regex( $regex ), $field, $matches ) ) {
						$headers[ $index ] = $special_key . $matches[1];
						break;
					}
				}
			}
		}

		return $headers;
	}

	/**
	 * Sanitize special column name regex.
	 *
	 * @internal
	 * @param  string $value Raw special column name.
	 * @return string
	 */
	public static function sanitize_special_column_name_regex( $value ) {
		return '/' . str_replace( array( '%d', '%s' ), '(.*)', trim( quotemeta( $value ) ) ) . '/';
	}

	/**
	 * Returns a valid cover block with an image, if one exists, or background as a fallback.
	 *
	 * @internal
	 * @param  array $image Image to use for the cover block. Should contain a media ID and image URL.
	 * @return string Block content.
	 */
	private static function get_homepage_cover_block( $image ) {
		$shop_url = get_permalink( wc_get_page_id( 'shop' ) );
		if ( ! empty( $image['url'] ) && ! empty( $image['id'] ) ) {
			return '<!-- wp:cover {"url":"' . esc_url( $image['url'] ) . '","id":' . intval( $image['id'] ) . ',"dimRatio":0} -->
			<div class="wp-block-cover" style="background-image:url(' . esc_url( $image['url'] ) . ')"><div class="wp-block-cover__inner-container"><!-- wp:paragraph {"align":"center","placeholder":"' . __( 'Write title…', 'woocommerce' ) . '","textColor":"white","fontSize":"large"} -->
			<p class="has-text-align-center has-large-font-size">' . __( 'Welcome to the store', 'woocommerce' ) . '</p>
			<!-- /wp:paragraph -->

			<!-- wp:paragraph {"align":"center","textColor":"white"} -->
			<p class="has-text-color has-text-align-center">' . __( 'Write a short welcome message here', 'woocommerce' ) . '</p>
			<!-- /wp:paragraph -->

			<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
			<div class="wp-block-buttons"><!-- wp:button -->
			<div class="wp-block-button"><a class="wp-block-button__link" href="' . esc_url( $shop_url ) . '">' . __( 'Go shopping', 'woocommerce' ) . '</a></div>
			<!-- /wp:button --></div>
			<!-- /wp:buttons --></div></div>
			<!-- /wp:cover -->';
		}

		return '<!-- wp:cover {"dimRatio":0} -->
		<div class="wp-block-cover"><div class="wp-block-cover__inner-container"><!-- wp:paragraph {"align":"center","placeholder":"' . __( 'Write title…', 'woocommerce' ) . '","textColor":"white","fontSize":"large"} -->
		<p class="has-text-color has-text-align-center has-large-font-size">' . __( 'Welcome to the store', 'woocommerce' ) . '</p>
		<!-- /wp:paragraph -->

		<!-- wp:paragraph {"align":"center","textColor":"white"} -->
		<p class="has-text-color has-text-align-center">' . __( 'Write a short welcome message here', 'woocommerce' ) . '</p>
		<!-- /wp:paragraph -->

		<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
		<div class="wp-block-buttons"><!-- wp:button -->
		<div class="wp-block-button"><a class="wp-block-button__link" href="' . esc_url( $shop_url ) . '">' . __( 'Go shopping', 'woocommerce' ) . '</a></div>
		<!-- /wp:button --></div>
		<!-- /wp:buttons --></div></div>
		<!-- /wp:cover -->';
	}

	/**
	 * Returns a valid media block with an image, if one exists, or a uninitialized media block the user can set.
	 *
	 * @internal
	 * @param  array  $image Image to use for the cover block. Should contain a media ID and image URL.
	 * @param  string $align If the image should be aligned to the left or right.
	 * @return string Block content.
	 */
	private static function get_homepage_media_block( $image, $align = 'left' ) {
		$media_position = 'right' === $align ? '"mediaPosition":"right",' : '';
		$css_class      = 'right' === $align ? ' has-media-on-the-right' : '';

		if ( ! empty( $image['url'] ) && ! empty( $image['id'] ) ) {
			return '<!-- wp:media-text {' . $media_position . '"mediaId":' . intval( $image['id'] ) . ',"mediaType":"image"} -->
			<div class="wp-block-media-text alignwide' . $css_class . '""><figure class="wp-block-media-text__media"><img src="' . esc_url( $image['url'] ) . '" alt="" class="wp-image-' . intval( $image['id'] ) . '"/></figure><div class="wp-block-media-text__content"><!-- wp:paragraph {"placeholder":"' . __( 'Content…', 'woocommerce' ) . '","fontSize":"large"} -->
			<p class="has-large-font-size"></p>
			<!-- /wp:paragraph --></div></div>
			<!-- /wp:media-text -->';
		}

		return '<!-- wp:media-text {' . $media_position . '} -->
		<div class="wp-block-media-text alignwide' . $css_class . '"><figure class="wp-block-media-text__media"></figure><div class="wp-block-media-text__content"><!-- wp:paragraph {"placeholder":"' . __( 'Content…', 'woocommerce' ) . '","fontSize":"large"} -->
		<p class="has-large-font-size"></p>
		<!-- /wp:paragraph --></div></div>
		<!-- /wp:media-text -->';
	}

	/**
	 * Returns a homepage template to be inserted into a post. A different template will be used depending on the number of products.
	 *
	 * @internal
	 * @param int $post_id ID of the homepage template.
	 * @return string Template contents.
	 */
	private static function get_homepage_template( $post_id ) {
		$products = wp_count_posts( 'product' );
		if ( $products->publish >= 4 ) {
			$images   = self::sideload_homepage_images( $post_id, 1 );
			$image_1  = ! empty( $images[0] ) ? $images[0] : '';
			$template = self::get_homepage_cover_block( $image_1 ) . '
				<!-- wp:heading {"align":"center"} -->
				<h2 style="text-align:center">' . __( 'Shop by Category', 'woocommerce' ) . '</h2>
				<!-- /wp:heading -->
				<!-- wp:shortcode -->
				[product_categories number="0" parent="0"]
				<!-- /wp:shortcode -->
				<!-- wp:heading {"align":"center"} -->
				<h2 style="text-align:center">' . __( 'New In', 'woocommerce' ) . '</h2>
				<!-- /wp:heading -->
				<!-- wp:woocommerce/product-new {"columns":4} /-->
				<!-- wp:heading {"align":"center"} -->
				<h2 style="text-align:center">' . __( 'Fan Favorites', 'woocommerce' ) . '</h2>
				<!-- /wp:heading -->
				<!-- wp:woocommerce/product-top-rated {"columns":4} /-->
				<!-- wp:heading {"align":"center"} -->
				<h2 style="text-align:center">' . __( 'On Sale', 'woocommerce' ) . '</h2>
				<!-- /wp:heading -->
				<!-- wp:woocommerce/product-on-sale {"columns":4} /-->
				<!-- wp:heading {"align":"center"} -->
				<h2 style="text-align:center">' . __( 'Best Sellers', 'woocommerce' ) . '</h2>
				<!-- /wp:heading -->
				<!-- wp:woocommerce/product-best-sellers {"columns":4} /-->
			';

			/**
			 * Modify the template/content of the default homepage.
			 *
			 * @param string $template The default homepage template.
			 */
			return apply_filters( 'woocommerce_admin_onboarding_homepage_template', $template );
		}

		$images   = self::sideload_homepage_images( $post_id, 3 );
		$image_1  = ! empty( $images[0] ) ? $images[0] : '';
		$image_2  = ! empty( $images[1] ) ? $images[1] : '';
		$image_3  = ! empty( $images[2] ) ? $images[2] : '';
		$template = self::get_homepage_cover_block( $image_1 ) . '
		<!-- wp:heading {"align":"center"} -->
		<h2 style="text-align:center">' . __( 'New Products', 'woocommerce' ) . '</h2>
		<!-- /wp:heading -->

		<!-- wp:woocommerce/product-new /--> ' .

		self::get_homepage_media_block( $image_1, 'right' ) .
		self::get_homepage_media_block( $image_2, 'left' ) .
		self::get_homepage_media_block( $image_3, 'right' ) . '

		<!-- wp:woocommerce/featured-product /-->';

		/** This filter is documented in src/API/OnboardingTasks.php. */
		return apply_filters( 'woocommerce_admin_onboarding_homepage_template', $template );
	}

	/**
	 * Gets the possible industry images from the plugin folder for sideloading. If an image doesn't exist, other.jpg is used a fallback.
	 *
	 * @internal
	 * @return array An array of images by industry.
	 */
	private static function get_available_homepage_images() {
		$industry_images = array();
		$industries      = OnboardingIndustries::get_allowed_industries();
		foreach ( $industries as $industry_slug => $label ) {
			$industry_images[ $industry_slug ] = apply_filters( 'woocommerce_admin_onboarding_industry_image', WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/other-small.jpg', $industry_slug );
		}
		return $industry_images;
	}

	/**
	 * Uploads a number of images to a homepage template, depending on the selected industry from the profile wizard.
	 *
	 * @internal
	 * @param  int $post_id ID of the homepage template.
	 * @param  int $number_of_images The number of images that should be sideloaded (depending on how many media slots are in the template).
	 * @return array An array of images that have been attached to the post.
	 */
	private static function sideload_homepage_images( $post_id, $number_of_images ) {
		$profile            = get_option( OnboardingProfile::DATA_OPTION, array() );
		$images_to_sideload = array();
		$available_images   = self::get_available_homepage_images();

		require_once ABSPATH . 'wp-admin/includes/image.php';
		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/media.php';

		if ( ! empty( $profile['industry'] ) ) {
			foreach ( $profile['industry'] as $selected_industry ) {
				if ( is_string( $selected_industry ) ) {
					$industry_slug = $selected_industry;
				} elseif ( is_array( $selected_industry ) && ! empty( $selected_industry['slug'] ) ) {
					$industry_slug = $selected_industry['slug'];
				} else {
					continue;
				}
				// Capture the first industry for use in our minimum images logic.
				$first_industry       = isset( $first_industry ) ? $first_industry : $industry_slug;
				$images_to_sideload[] = ! empty( $available_images[ $industry_slug ] ) ? $available_images[ $industry_slug ] : $available_images['other'];
			}
		}

		// Make sure we have at least {$number_of_images} images.
		if ( count( $images_to_sideload ) < $number_of_images ) {
			for ( $i = count( $images_to_sideload ); $i < $number_of_images; $i++ ) {
				// Fill up missing image slots with the first selected industry, or other.
				$industry             = isset( $first_industry ) ? $first_industry : 'other';
				$images_to_sideload[] = empty( $available_images[ $industry ] ) ? $available_images['other'] : $available_images[ $industry ];
			}
		}

		$already_sideloaded = array();
		$images_for_post    = array();
		foreach ( $images_to_sideload as $image ) {
			// Avoid uploading two of the same image, if an image is repeated.
			if ( ! empty( $already_sideloaded[ $image ] ) ) {
				$images_for_post[] = $already_sideloaded[ $image ];
				continue;
			}

			$sideload_id = \media_sideload_image( $image, $post_id, null, 'id' );
			if ( ! is_wp_error( $sideload_id ) ) {
				$sideload_url                 = wp_get_attachment_url( $sideload_id );
				$already_sideloaded[ $image ] = array(
					'id'  => $sideload_id,
					'url' => $sideload_url,
				);
				$images_for_post[]            = $already_sideloaded[ $image ];
			}
		}

		return $images_for_post;
	}

	/**
	 * Create a homepage from a template.
	 *
	 * @return WP_Error|array
	 */
	public static function create_homepage() {
		$post_id = wp_insert_post(
			array(
				'post_title'   => __( 'Homepage', 'woocommerce' ),
				'post_type'    => 'page',
				'post_status'  => 'publish',
				'post_content' => '', // Template content is updated below, so images can be attached to the post.
			)
		);

		if ( ! is_wp_error( $post_id ) && 0 < $post_id ) {

			$template = self::get_homepage_template( $post_id );
			wp_update_post(
				array(
					'ID'           => $post_id,
					'post_content' => $template,
				)
			);

			update_option( 'show_on_front', 'page' );
			update_option( 'page_on_front', $post_id );
			update_option( 'woocommerce_onboarding_homepage_post_id', $post_id );

			// Use the full width template on stores using Storefront.
			if ( 'storefront' === get_stylesheet() ) {
				update_post_meta( $post_id, '_wp_page_template', 'template-fullwidth.php' );
			}

			return array(
				'status'         => 'success',
				'message'        => __( 'Homepage created', 'woocommerce' ),
				'post_id'        => $post_id,
				'edit_post_link' => htmlspecialchars_decode( get_edit_post_link( $post_id ) ),
			);
		} else {
			return $post_id;
		}
	}

	/**
	 * Get the query params for task lists.
	 *
	 * @return array
	 */
	public function get_task_list_params() {
		$params                   = array();
		$params['ids']            = array(
			'description'       => __( 'Optional parameter to get only specific task lists by id.', 'woocommerce' ),
			'type'              => 'array',
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
			'items'             => array(
				'enum' => TaskLists::get_list_ids(),
				'type' => 'string',
			),
		);
		$params['extended_tasks'] = array(
			'description'       => __( 'List of extended deprecated tasks from the client side filter.', 'woocommerce' ),
			'type'              => 'array',
			'validate_callback' => function( $param, $request, $key ) {
				$has_valid_keys = true;
				foreach ( $param as $task ) {
					if ( $has_valid_keys ) {
						$has_valid_keys = array_key_exists( 'list_id', $task ) && array_key_exists( 'id', $task );
					}
				}
				return $has_valid_keys;
			},
		);
		return $params;
	}

	/**
	 * Get the onboarding tasks.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_tasks( $request ) {
		$extended_tasks = $request->get_param( 'extended_tasks' );
		$task_list_ids  = $request->get_param( 'ids' );

		TaskLists::maybe_add_extended_tasks( $extended_tasks );

		$lists = is_array( $task_list_ids ) && count( $task_list_ids ) > 0 ? TaskLists::get_lists_by_ids( $task_list_ids ) : TaskLists::get_lists();

		$json = array_map(
			function( $list ) {
				return $list->sort_tasks()->get_json();
			},
			$lists
		);

		return rest_ensure_response( array_values( apply_filters( 'woocommerce_admin_onboarding_tasks', $json ) ) );
	}

	/**
	 * Dismiss a single task.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function dismiss_task( $request ) {
		$id   = $request->get_param( 'id' );
		$task = TaskLists::get_task( $id );

		if ( ! $task && $id ) {
			$task = new DeprecatedExtendedTask(
				array(
					'id'             => $id,
					'is_dismissable' => true,
				)
			);
		}

		if ( ! $task || ! $task->is_dismissable() ) {
			return new \WP_Error(
				'woocommerce_rest_invalid_task',
				__( 'Sorry, no dismissable task with that ID was found.', 'woocommerce' ),
				array(
					'status' => 404,
				)
			);
		}

		$task->dismiss();
		return rest_ensure_response( $task->get_json() );
	}

	/**
	 * Undo dismissal of a single task.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function undo_dismiss_task( $request ) {
		$id   = $request->get_param( 'id' );
		$task = TaskLists::get_task( $id );

		if ( ! $task && $id ) {
			$task = new DeprecatedExtendedTask(
				array(
					'id'             => $id,
					'is_dismissable' => true,
				)
			);
		}

		if ( ! $task || ! $task->is_dismissable() ) {
			return new \WP_Error(
				'woocommerce_rest_invalid_task',
				__( 'Sorry, no dismissable task with that ID was found.', 'woocommerce' ),
				array(
					'status' => 404,
				)
			);
		}

		$task->undo_dismiss();

		return rest_ensure_response( $task->get_json() );
	}

	/**
	 * Snooze an onboarding task.
	 *
	 * @param WP_REST_Request $request Request data.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function snooze_task( $request ) {
		$task_id      = $request->get_param( 'id' );
		$task_list_id = $request->get_param( 'task_list_id' );
		$duration     = $request->get_param( 'duration' );

		$task = TaskLists::get_task( $task_id, $task_list_id );

		if ( ! $task && $task_id ) {
			$task = new DeprecatedExtendedTask(
				array(
					'id'            => $task_id,
					'is_snoozeable' => true,
				)
			);
		}

		if ( ! $task || ! $task->is_snoozeable() ) {
			return new \WP_Error(
				'woocommerce_rest_invalid_task',
				__( 'Sorry, no snoozeable task with that ID was found.', 'woocommerce' ),
				array(
					'status' => 404,
				)
			);
		}

		$task->snooze( isset( $duration ) ? $duration : 'day' );
		return rest_ensure_response( $task->get_json() );
	}

	/**
	 * Undo snooze of a single task.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function undo_snooze_task( $request ) {
		$id   = $request->get_param( 'id' );
		$task = TaskLists::get_task( $id );

		if ( ! $task && $id ) {
			$task = new DeprecatedExtendedTask(
				array(
					'id'            => $id,
					'is_snoozeable' => true,
				)
			);
		}

		if ( ! $task || ! $task->is_snoozeable() ) {
			return new \WP_Error(
				'woocommerce_rest_invalid_task',
				__( 'Sorry, no snoozeable task with that ID was found.', 'woocommerce' ),
				array(
					'status' => 404,
				)
			);
		}

		$task->undo_snooze();
		return rest_ensure_response( $task->get_json() );
	}

	/**
	 * Hide a task list.
	 *
	 * @param WP_REST_Request $request Request data.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function hide_task_list( $request ) {
		$id        = $request->get_param( 'id' );
		$task_list = TaskLists::get_list( $id );

		if ( ! $task_list ) {
			return new \WP_Error(
				'woocommerce_rest_invalid_task_list',
				__( 'Sorry, that task list was not found', 'woocommerce' ),
				array(
					'status' => 404,
				)
			);
		}

		$update = $task_list->hide();
		$json   = $task_list->get_json();

		return rest_ensure_response( $json );
	}

	/**
	 * Unhide a task list.
	 *
	 * @param WP_REST_Request $request Request data.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function unhide_task_list( $request ) {
		$id        = $request->get_param( 'id' );
		$task_list = TaskLists::get_list( $id );

		if ( ! $task_list ) {
			return new \WP_Error(
				'woocommerce_tasks_invalid_task_list',
				__( 'Sorry, that task list was not found', 'woocommerce' ),
				array(
					'status' => 404,
				)
			);
		}

		$update = $task_list->unhide();
		$json   = $task_list->get_json();

		return rest_ensure_response( $json );
	}

	/**
	 * Action a single task.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function action_task( $request ) {
		$id   = $request->get_param( 'id' );
		$task = TaskLists::get_task( $id );

		if ( ! $task && $id ) {
			$task = new DeprecatedExtendedTask(
				array(
					'id' => $id,
				)
			);
		}

		if ( ! $task ) {
			return new \WP_Error(
				'woocommerce_rest_invalid_task',
				__( 'Sorry, no task with that ID was found.', 'woocommerce' ),
				array(
					'status' => 404,
				)
			);
		}

		$task->mark_actioned();
		return rest_ensure_response( $task->get_json() );
	}

}
Admin/API/CustomAttributeTraits.php000064400000006634152333011040013244 0ustar00<?php
/**
 * Traits for handling custom product attributes and their terms.
 */

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * CustomAttributeTraits class.
 *
 * @internal
 */
trait CustomAttributeTraits {
	/**
	 * Get a single attribute by its slug.
	 *
	 * @internal
	 * @param string $slug The attribute slug.
	 * @return WP_Error|object The matching attribute object or WP_Error if not found.
	 */
	public function get_custom_attribute_by_slug( $slug ) {
		$matching_attributes = $this->get_custom_attributes( array( 'slug' => $slug ) );

		if ( empty( $matching_attributes ) ) {
			return new \WP_Error(
				'woocommerce_rest_product_attribute_not_found',
				__( 'No product attribute with that slug was found.', 'woocommerce' ),
				array( 'status' => 404 )
			);
		}

		foreach ( $matching_attributes as $attribute_key => $attribute_value ) {
			return array( $attribute_key => $attribute_value );
		}
	}

	/**
	 * Query custom attributes by name or slug.
	 *
	 * @param string $args Search arguments, either name or slug.
	 * @return array Matching attributes, formatted for response.
	 */
	protected function get_custom_attributes( $args ) {
		global $wpdb;

		$args = wp_parse_args(
			$args,
			array(
				'name' => '',
				'slug' => '',
			)
		);

		if ( empty( $args['name'] ) && empty( $args['slug'] ) ) {
			return array();
		}

		$mode = $args['name'] ? 'name' : 'slug';

		if ( 'name' === $mode ) {
			$name = $args['name'];
			// Get as close as we can to matching the name property of custom attributes using SQL.
			$like = '%"name";s:%:"%' . $wpdb->esc_like( $name ) . '%"%';
		} else {
			$slug = sanitize_title_for_query( $args['slug'] );
			// Get as close as we can to matching the slug property of custom attributes using SQL.
			$like = '%s:' . strlen( $slug ) . ':"' . $slug . '";a:6:{%';
		}

		// Find all serialized product attributes with names like the search string.
		$query_results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT meta_value
				FROM {$wpdb->postmeta}
				WHERE meta_key = '_product_attributes'
				AND meta_value LIKE %s
				LIMIT 100",
				$like
			),
			ARRAY_A
		);

		$custom_attributes = array();

		foreach ( $query_results as $raw_product_attributes ) {

			$meta_attributes = maybe_unserialize( $raw_product_attributes['meta_value'] );

			if ( empty( $meta_attributes ) || ! is_array( $meta_attributes ) ) {
				continue;
			}

			foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) {
				$meta_value = array_merge(
					array(
						'name'        => '',
						'is_taxonomy' => 0,
					),
					(array) $meta_attribute_value
				);

				// Skip non-custom attributes.
				if ( ! empty( $meta_value['is_taxonomy'] ) ) {
					continue;
				}

				// Skip custom attributes that didn't match the query.
				// (There can be any number of attributes in the meta value).
				if ( ( 'name' === $mode ) && ( false === stripos( $meta_value['name'], $name ) ) ) {
					continue;
				}

				if ( ( 'slug' === $mode ) && ( $meta_attribute_key !== $slug ) ) {
					continue;
				}

				// Combine all values when there are multiple matching custom attributes.
				if ( isset( $custom_attributes[ $meta_attribute_key ] ) ) {
					$custom_attributes[ $meta_attribute_key ]['value'] .= ' ' . WC_DELIMITER . ' ' . $meta_value['value'];
				} else {
					$custom_attributes[ $meta_attribute_key ] = $meta_attribute_value;
				}
			}
		}

		return $custom_attributes;
	}
}
Admin/DataSourcePoller.php000064400000014001152333011040011501 0ustar00<?php

namespace Automattic\WooCommerce\Admin;

/**
 * Specs data source poller class.
 * This handles polling specs from JSON endpoints, and
 * stores the specs in to the database as an option.
 */
abstract class DataSourcePoller {

	/**
	 * Get class instance.
	 */
	abstract public static function get_instance();

	/**
	 * Name of data sources filter.
	 */
	const FILTER_NAME = 'data_source_poller_data_sources';

	/**
	 * Name of data source specs filter.
	 */
	const FILTER_NAME_SPECS = 'data_source_poller_specs';

	/**
	 * Id of DataSourcePoller.
	 *
	 * @var string
	 */
	protected $id = array();

	/**
	 * Default data sources array.
	 *
	 * @var array
	 */
	protected $data_sources = array();

	/**
	 * Default args.
	 *
	 * @var array
	 */
	protected $args = array();

	/**
	 * The logger instance.
	 *
	 * @var WC_Logger|null
	 */
	protected static $logger = null;

	/**
	 * Constructor.
	 *
	 * @param string $id id of DataSourcePoller.
	 * @param array  $data_sources urls for data sources.
	 * @param array  $args Options for DataSourcePoller.
	 */
	public function __construct( $id, $data_sources = array(), $args = array() ) {
		$this->data_sources = $data_sources;
		$this->id           = $id;

		$arg_defaults = array(
			'spec_key'         => 'id',
			'transient_name'   => 'woocommerce_admin_' . $id . '_specs',
			'transient_expiry' => 7 * DAY_IN_SECONDS,
		);
		$this->args   = wp_parse_args( $args, $arg_defaults );
	}

	/**
	 * Get the logger instance.
	 *
	 * @return WC_Logger
	 */
	protected static function get_logger() {
		if ( is_null( self::$logger ) ) {
			self::$logger = wc_get_logger();
		}

		return self::$logger;
	}

	/**
	 * Returns the key identifier of spec, this can easily be overwritten. Defaults to id.
	 *
	 * @param mixed $spec a JSON parsed spec coming from the JSON feed.
	 * @return string|boolean
	 */
	protected function get_spec_key( $spec ) {
		$key = $this->args['spec_key'];
		if ( isset( $spec->$key ) ) {
			return $spec->$key;
		}
		return false;
	}

	/**
	 * Reads the data sources for specs and persists those specs.
	 *
	 * @return array list of specs.
	 */
	public function get_specs_from_data_sources() {
		$locale      = get_user_locale();
		$specs_group = get_transient( $this->args['transient_name'] ) ?? array();
		$specs       = isset( $specs_group[ $locale ] ) ? $specs_group[ $locale ] : array();

		if ( ! is_array( $specs ) || empty( $specs ) ) {
			$this->read_specs_from_data_sources();
			$specs_group = get_transient( $this->args['transient_name'] );
			$specs       = isset( $specs_group[ $locale ] ) ? $specs_group[ $locale ] : array();
		}
		$specs = apply_filters( self::FILTER_NAME_SPECS, $specs, $this->id );
		return $specs !== false ? $specs : array();
	}

	/**
	 * Reads the data sources for specs and persists those specs.
	 *
	 * @return bool Whether any specs were read.
	 */
	public function read_specs_from_data_sources() {
		$specs        = array();
		$data_sources = apply_filters( self::FILTER_NAME, $this->data_sources, $this->id );

		// Note that this merges the specs from the data sources based on the
		// id - last one wins.
		foreach ( $data_sources as $url ) {
			$specs_from_data_source = self::read_data_source( $url );
			$this->merge_specs( $specs_from_data_source, $specs, $url );
		}

		$specs_group            = get_transient( $this->args['transient_name'] ) ?? array();
		$locale                 = get_user_locale();
		$specs_group[ $locale ] = $specs;
		// Persist the specs as a transient.
		set_transient(
			$this->args['transient_name'],
			$specs_group,
			$this->args['transient_expiry']
		);
		return count( $specs ) !== 0;
	}

	/**
	 * Delete the specs transient.
	 *
	 * @return bool success of failure of transient deletion.
	 */
	public function delete_specs_transient() {
		return delete_transient( $this->args['transient_name'] );
	}

	/**
	 * Read a single data source and return the read specs
	 *
	 * @param string $url The URL to read the specs from.
	 *
	 * @return array The specs that have been read from the data source.
	 */
	protected static function read_data_source( $url ) {
		$logger_context = array( 'source' => $url );
		$logger         = self::get_logger();
		$response       = wp_remote_get(
			add_query_arg(
				'locale',
				get_user_locale(),
				$url
			),
			array(
				'user-agent' => 'WooCommerce/' . WC_VERSION . '; ' . home_url( '/' ),
			)
		);

		if ( is_wp_error( $response ) || ! isset( $response['body'] ) ) {
			$logger->error(
				'Error getting data feed',
				$logger_context
			);
			// phpcs:ignore
			$logger->error( print_r( $response, true ), $logger_context );

			return [];
		}

		$body  = $response['body'];
		$specs = json_decode( $body );

		if ( $specs === null ) {
			$logger->error(
				'Empty response in data feed',
				$logger_context
			);

			return [];
		}

		if ( ! is_array( $specs ) ) {
			$logger->error(
				'Data feed is not an array',
				$logger_context
			);

			return [];
		}

		return $specs;
	}

	/**
	 * Merge the specs.
	 *
	 * @param Array  $specs_to_merge_in The specs to merge in to $specs.
	 * @param Array  $specs             The list of specs being merged into.
	 * @param string $url               The url of the feed being merged in (for error reporting).
	 */
	protected function merge_specs( $specs_to_merge_in, &$specs, $url ) {
		foreach ( $specs_to_merge_in as $spec ) {
			if ( ! $this->validate_spec( $spec, $url ) ) {
				continue;
			}

			$id           = $this->get_spec_key( $spec );
			$specs[ $id ] = $spec;
		}
	}

	/**
	 * Validate the spec.
	 *
	 * @param object $spec The spec to validate.
	 * @param string $url  The url of the feed that provided the spec.
	 *
	 * @return bool The result of the validation.
	 */
	protected function validate_spec( $spec, $url ) {
		$logger         = self::get_logger();
		$logger_context = array( 'source' => $url );

		if ( ! $this->get_spec_key( $spec ) ) {
			$logger->error(
				'Spec is invalid because the id is missing in feed',
				$logger_context
			);
			// phpcs:ignore
			$logger->error( print_r( $spec, true ), $logger_context );

			return false;
		}

		return true;
	}
}
Admin/DateTimeProvider/CurrentDateTimeProvider.php000064400000000763152333011040016264 0ustar00<?php
/**
 * A provider for getting the current DateTime.
 */

namespace Automattic\WooCommerce\Admin\DateTimeProvider;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\DateTimeProvider\DateTimeProviderInterface;

/**
 * Current DateTime Provider.
 *
 * Uses the current DateTime.
 */
class CurrentDateTimeProvider implements DateTimeProviderInterface {
	/**
	 * Returns the current DateTime.
	 *
	 * @return DateTime
	 */
	public function get_now() {
		return new \DateTime();
	}
}
Admin/DateTimeProvider/DateTimeProviderInterface.php000064400000000602152333011040016532 0ustar00<?php
/**
 * Interface for a provider for getting the current DateTime,
 * designed to be mockable for unit tests.
 */

namespace Automattic\WooCommerce\Admin\DateTimeProvider;

defined( 'ABSPATH' ) || exit;

/**
 * DateTime Provider Interface.
 */
interface DateTimeProviderInterface {
	/**
	 * Returns the current DateTime.
	 *
	 * @return DateTime
	 */
	public function get_now();
}
Admin/ReportCSVExporter.php000064400000022773152333011040011670 0ustar00<?php
/**
 * Handles reports CSV export batches.
 */

namespace Automattic\WooCommerce\Admin;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;

/**
 * Include dependencies.
 */
if ( ! class_exists( 'WC_CSV_Batch_Exporter', false ) ) {
	include_once WC_ABSPATH . 'includes/export/abstract-wc-csv-batch-exporter.php';
}

/**
 * ReportCSVExporter Class.
 */
class ReportCSVExporter extends \WC_CSV_Batch_Exporter {
	/**
	 * Type of report being exported.
	 *
	 * @var string
	 */
	protected $report_type;

	/**
	 * Parameters for the report query.
	 *
	 * @var array
	 */
	protected $report_args;

	/**
	 * REST controller for the report.
	 *
	 * @var WC_REST_Reports_Controller
	 */
	protected $controller;

	/**
	 * Constructor.
	 *
	 * @param string $type Report type. E.g. 'customers'.
	 * @param array  $args Report parameters.
	 */
	public function __construct( $type = false, $args = array() ) {
		parent::__construct();

		self::maybe_create_directory();

		if ( ! empty( $type ) ) {
			$this->set_report_type( $type );
			$this->set_column_names( $this->get_report_columns() );
		}

		if ( ! empty( $args ) ) {
			$this->set_report_args( $args );
		}
	}

	/**
	 * Create the directory for reports if it does not yet exist.
	 */
	public static function maybe_create_directory() {
		$reports_dir = self::get_reports_directory();

		$files = array(
			array(
				'base'    => $reports_dir,
				'file'    => '.htaccess',
				'content' => 'DirectoryIndex index.php index.html' . PHP_EOL . 'deny from all',
			),
			array(
				'base'    => $reports_dir,
				'file'    => 'index.html',
				'content' => '',
			),
		);

		foreach ( $files as $file ) {
			if ( ! file_exists( trailingslashit( $file['base'] ) ) ) {
				wp_mkdir_p( $file['base'] );
			}
			if ( ! file_exists( trailingslashit( $file['base'] ) . $file['file'] ) ) {
				$file_handle = @fopen( trailingslashit( $file['base'] ) . $file['file'], 'wb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen
				if ( $file_handle ) {
					fwrite( $file_handle, $file['content'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite
					fclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
				}
			}
		}
	}

	/**
	 * Get report uploads directory.
	 *
	 * @return string
	 */
	public static function get_reports_directory() {
		$upload_dir = wp_upload_dir();
		return trailingslashit( $upload_dir['basedir'] ) . 'woocommerce_uploads/reports/';
	}

	/**
	 * Get file path to export to.
	 *
	 * @return string
	 */
	protected function get_file_path() {
		return self::get_reports_directory() . $this->get_filename();
	}


	/**
	 * Setter for report type.
	 *
	 * @param string $type The report type. E.g. customers.
	 */
	public function set_report_type( $type ) {
		$this->report_type = $type;
		$this->export_type = "admin_{$type}_report";
		$this->filename    = "wc-{$type}-report-export";
		$this->controller  = $this->map_report_controller();
	}

	/**
	 * Setter for report args.
	 *
	 * @param array $args The report args.
	 */
	public function set_report_args( $args ) {
		// Use our own internal limit and include all extended info.
		$report_args = array_merge(
			$args,
			array(
				'per_page'      => $this->get_limit(),
				'extended_info' => true,
			)
		);

		// Should this happen externally?
		if ( isset( $report_args['page'] ) ) {
			$this->set_page( $report_args['page'] );
		}

		$this->report_args = $report_args;
	}

	/**
	 * Get a REST controller instance for the report type.
	 *
	 * @return bool|WC_REST_Reports_Controller Report controller instance or boolean false on error.
	 */
	protected function map_report_controller() {
		// @todo - Add filter to this list.
		$controller_map = array(
			'products'   => 'Automattic\WooCommerce\Admin\API\Reports\Products\Controller',
			'variations' => 'Automattic\WooCommerce\Admin\API\Reports\Variations\Controller',
			'orders'     => 'Automattic\WooCommerce\Admin\API\Reports\Orders\Controller',
			'categories' => 'Automattic\WooCommerce\Admin\API\Reports\Categories\Controller',
			'taxes'      => 'Automattic\WooCommerce\Admin\API\Reports\Taxes\Controller',
			'coupons'    => 'Automattic\WooCommerce\Admin\API\Reports\Coupons\Controller',
			'stock'      => 'Automattic\WooCommerce\Admin\API\Reports\Stock\Controller',
			'downloads'  => 'Automattic\WooCommerce\Admin\API\Reports\Downloads\Controller',
			'customers'  => 'Automattic\WooCommerce\Admin\API\Reports\Customers\Controller',
			'revenue'    => 'Automattic\WooCommerce\Admin\API\Reports\Revenue\Stats\Controller',
		);

		if ( isset( $controller_map[ $this->report_type ] ) ) {
			// Load the controllers if accessing outside the REST API.
			return new $controller_map[ $this->report_type ]();
		}

		// Should this do something else?
		return false;
	}

	/**
	 * Get the report columns from the controller.
	 *
	 * @return array Array of report column names.
	 */
	protected function get_report_columns() {
		// Default to the report's defined export columns.
		if ( $this->controller instanceof ExportableInterface ) {
			return $this->controller->get_export_columns();
		}

		// Fallback to generating columns from the report schema.
		$report_columns = array();
		$report_schema  = $this->controller->get_item_schema();

		if ( isset( $report_schema['properties'] ) ) {
			foreach ( $report_schema['properties'] as $column_name => $column_info ) {
				// Expand extended info columns into export.
				if ( 'extended_info' === $column_name ) {
					// Remove columns with questionable CSV values, like markup.
					$extended_info  = array_diff( array_keys( $column_info ), array( 'image' ) );
					$report_columns = array_merge( $report_columns, $extended_info );
				} else {
					$report_columns[] = $column_name;
				}
			}
		}

		return $report_columns;
	}

	/**
	 * Get total % complete.
	 *
	 * Forces an int from parent::get_percent_complete(), which can return a float.
	 *
	 * @return int Percent complete.
	 */
	public function get_percent_complete() {
		return intval( parent::get_percent_complete() );
	}

	/**
	 * Get total number of rows in export.
	 *
	 * @return int Number of rows to export.
	 */
	public function get_total_rows() {
		return $this->total_rows;
	}

	/**
	 * Prepare data for export.
	 */
	public function prepare_data_to_export() {
		$request  = new \WP_REST_Request( 'GET', "/wc-analytics/reports/{$this->report_type}" );
		$params   = $this->controller->get_collection_params();
		$defaults = array();

		foreach ( $params as $arg => $options ) {
			if ( isset( $options['default'] ) ) {
				$defaults[ $arg ] = $options['default'];
			}
		}

		$request->set_attributes( array( 'args' => $params ) );
		$request->set_default_params( $defaults );
		$request->set_query_params( $this->report_args );
		$request->sanitize_params();

		// Does the controller have an export-specific item retrieval method?
		// @todo - Potentially revisit. This is only for /revenue/stats/.
		if ( is_callable( array( $this->controller, 'get_export_items' ) ) ) {
			$response = $this->controller->get_export_items( $request );
		} else {
			$response = $this->controller->get_items( $request );
		}

		// Use WP_REST_Server::response_to_data() to embed links in data.
		add_filter( 'woocommerce_rest_check_permissions', '__return_true' );
		$rest_server = rest_get_server();
		$report_data = $rest_server->response_to_data( $response, true );
		remove_filter( 'woocommerce_rest_check_permissions', '__return_true' );

		$report_meta      = $response->get_headers();
		$this->total_rows = $report_meta['X-WP-Total'];
		$this->row_data   = array_map( array( $this, 'generate_row_data' ), $report_data );
	}

	/**
	 * Generate row data from a raw report item.
	 *
	 * @param object $item Report item data.
	 * @return array CSV row data.
	 */
	protected function get_raw_row_data( $item ) {
		$columns = $this->get_column_names();
		$row     = array();

		// Expand extended info.
		if ( isset( $item['extended_info'] ) ) {
			// Pull extended info property from report item object.
			$extended_info = (array) $item['extended_info'];
			unset( $item['extended_info'] );

			// Merge extended info columns into report item object.
			$item = array_merge( $item, $extended_info );
		}

		foreach ( $columns as $column_id => $column_name ) {
			$value = isset( $item[ $column_name ] ) ? $item[ $column_name ] : null;

			if ( has_filter( "woocommerce_export_{$this->export_type}_column_{$column_name}" ) ) {
				// Filter for 3rd parties.
				$value = apply_filters( "woocommerce_export_{$this->export_type}_column_{$column_name}", '', $item );

			} elseif ( is_callable( array( $this, "get_column_value_{$column_name}" ) ) ) {
				// Handle special columns which don't map 1:1 to item data.
				$value = $this->{"get_column_value_{$column_name}"}( $item, $this->export_type );

			} elseif ( ! is_scalar( $value ) ) {
				// Ensure that the value is somewhat readable in CSV.
				$value = wp_json_encode( $value );
			}

			$row[ $column_id ] = $value;
		}

		return $row;
	}

	/**
	 * Get the export row for a given report item.
	 *
	 * @param object $item Report item data.
	 * @return array CSV row data.
	 */
	protected function generate_row_data( $item ) {
		// Default to the report's export method.
		if ( $this->controller instanceof ExportableInterface ) {
			$row = $this->controller->prepare_item_for_export( $item );
		} else {
			// Fallback to raw report data.
			$row = $this->get_raw_row_data( $item );
		}

		return apply_filters( "woocommerce_export_{$this->export_type}_row_data", $row, $item );
	}
}
Internal/Features/FeaturesController.php000064400000116322152333011040014426 0ustar00<?php
/**
 * FeaturesController class file
 */

namespace Automattic\WooCommerce\Internal\Features;

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\Admin\Analytics;
use Automattic\WooCommerce\Admin\Features\Navigation\Init;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\PluginUtil;

defined( 'ABSPATH' ) || exit;

/**
 * Class to define the WooCommerce features that can be enabled and disabled by admin users,
 * provides also a mechanism for WooCommerce plugins to declare that they are compatible
 * (or incompatible) with a given feature.
 */
class FeaturesController {

	use AccessiblePrivateMethods;

	public const FEATURE_ENABLED_CHANGED_ACTION = 'woocommerce_feature_enabled_changed';

	/**
	 * The existing feature definitions.
	 *
	 * @var array[]
	 */
	private $features;

	/**
	 * The registered compatibility info for WooCommerce plugins, with plugin names as keys.
	 *
	 * @var array
	 */
	private $compatibility_info_by_plugin;

	/**
	 * Ids of the legacy features (they existed before the features engine was implemented).
	 *
	 * @var array
	 */
	private $legacy_feature_ids;

	/**
	 * The registered compatibility info for WooCommerce plugins, with feature ids as keys.
	 *
	 * @var array
	 */
	private $compatibility_info_by_feature;

	/**
	 * The LegacyProxy instance to use.
	 *
	 * @var LegacyProxy
	 */
	private $proxy;

	/**
	 * The PluginUtil instance to use.
	 *
	 * @var PluginUtil
	 */
	private $plugin_util;

	/**
	 * Flag indicating that features will be enableable from the settings page
	 * even when they are incompatible with active plugins.
	 *
	 * @var bool
	 */
	private $force_allow_enabling_features = false;

	/**
	 * Flag indicating that plugins will be activable from the plugins page
	 * even when they are incompatible with enabled features.
	 *
	 * @var bool
	 */
	private $force_allow_enabling_plugins = false;

	/**
	 * Creates a new instance of the class.
	 */
	public function __construct() {
		$features = array(
			'analytics'           => array(
				'name'               => __( 'Analytics', 'woocommerce' ),
				'description'        => __( 'Enables WooCommerce Analytics', 'woocommerce' ),
				'is_experimental'    => false,
				'enabled_by_default' => true,
			),
			'new_navigation'      => array(
				'name'            => __( 'Navigation', 'woocommerce' ),
				'description'     => __( 'Adds the new WooCommerce navigation experience to the dashboard', 'woocommerce' ),
				'is_experimental' => false,
			),
			'custom_order_tables' => array(
				'name'            => __( 'High-Performance order storage (COT)', 'woocommerce' ),
				'description'     => __( 'Enable the high performance order storage feature.', 'woocommerce' ),
				'is_experimental' => true,
			),
		);

		$this->legacy_feature_ids = array( 'analytics', 'new_navigation' );

		$this->init_features( $features );

		self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
		self::add_filter( 'added_option', array( $this, 'process_added_option' ), 999, 3 );
		self::add_filter( 'woocommerce_get_sections_advanced', array( $this, 'add_features_section' ), 10, 1 );
		self::add_filter( 'woocommerce_get_settings_advanced', array( $this, 'add_feature_settings' ), 10, 2 );
		self::add_filter( 'deactivated_plugin', array( $this, 'handle_plugin_deactivation' ), 10, 1 );
		self::add_filter( 'all_plugins', array( $this, 'filter_plugins_list' ), 10, 1 );
		self::add_action( 'admin_notices', array( $this, 'display_notices_in_plugins_page' ), 10, 0 );
		self::add_action( 'after_plugin_row', array( $this, 'handle_plugin_list_rows' ), 10, 2 );
		self::add_action( 'current_screen', array( $this, 'enqueue_script_to_fix_plugin_list_html' ), 10, 1 );
		self::add_filter( 'views_plugins', array( $this, 'handle_plugins_page_views_list' ), 10, 1 );
	}

	/**
	 * Initialize the class according to the existing features.
	 *
	 * @param array $features Information about the existing features.
	 */
	private function init_features( array $features ) {
		$this->compatibility_info_by_plugin  = array();
		$this->compatibility_info_by_feature = array();

		$this->features = $features;

		foreach ( array_keys( $this->features ) as $feature_id ) {
			$this->compatibility_info_by_feature[ $feature_id ] = array(
				'compatible'   => array(),
				'incompatible' => array(),
			);
		}
	}

	/**
	 * Initialize the class instance.
	 *
	 * @internal
	 *
	 * @param LegacyProxy $proxy The instance of LegacyProxy to use.
	 * @param PluginUtil  $plugin_util The instance of PluginUtil to use.
	 */
	final public function init( LegacyProxy $proxy, PluginUtil $plugin_util ) {
		$this->proxy       = $proxy;
		$this->plugin_util = $plugin_util;
	}

	/**
	 * Get all the existing WooCommerce features.
	 *
	 * Returns an associative array where keys are unique feature ids
	 * and values are arrays with these keys:
	 *
	 * - name (string)
	 * - description (string)
	 * - is_experimental (bool)
	 * - is_enabled (bool) (only if $include_enabled_info is passed as true)
	 *
	 * @param bool $include_experimental Include also experimental/work in progress features in the list.
	 * @param bool $include_enabled_info True to include the 'is_enabled' field in the returned features info.
	 * @returns array An array of information about existing features.
	 */
	public function get_features( bool $include_experimental = false, bool $include_enabled_info = false ): array {
		$features = $this->features;

		if ( ! $include_experimental ) {
			$features = array_filter(
				$features,
				function( $feature ) {
					return ! $feature['is_experimental'];
				}
			);
		}

		if ( $include_enabled_info ) {
			foreach ( array_keys( $features ) as $feature_id ) {
				$is_enabled                            = $this->feature_is_enabled( $feature_id );
				$features[ $feature_id ]['is_enabled'] = $is_enabled;
			}
		}

		return $features;
	}

	/**
	 * Check if a given feature is currently enabled.
	 *
	 * @param  string $feature_id Unique feature id.
	 * @return bool True if the feature is enabled, false if not or if the feature doesn't exist.
	 */
	public function feature_is_enabled( string $feature_id ): bool {
		if ( ! $this->feature_exists( $feature_id ) ) {
			return false;
		}

		$default_value = $this->feature_is_enabled_by_default( $feature_id ) ? 'yes' : 'no';
		return 'yes' === get_option( $this->feature_enable_option_name( $feature_id ), $default_value );
	}

	/**
	 * Check if a given feature is enabled by default.
	 *
	 * @param string $feature_id Unique feature id.
	 * @return boolean TRUE if the feature is enabled by default, FALSE otherwise.
	 */
	private function feature_is_enabled_by_default( string $feature_id ): bool {
		return ! empty( $this->features[ $feature_id ]['enabled_by_default'] );
	}

	/**
	 * Change the enabled/disabled status of a feature.
	 *
	 * @param string $feature_id Unique feature id.
	 * @param bool   $enable True to enable the feature, false to disable it.
	 * @return bool True on success, false if feature doesn't exist or the new value is the same as the old value.
	 */
	public function change_feature_enable( string $feature_id, bool $enable ): bool {
		if ( ! $this->feature_exists( $feature_id ) ) {
			return false;
		}

		return update_option( $this->feature_enable_option_name( $feature_id ), $enable ? 'yes' : 'no' );
	}

	/**
	 * Declare (in)compatibility with a given feature for a given plugin.
	 *
	 * This method MUST be executed from inside a handler for the 'before_woocommerce_init' hook.
	 *
	 * The plugin name is expected to be in the form 'directory/file.php' and be one of the keys
	 * of the array returned by 'get_plugins', but this won't be checked. Plugins are expected to use
	 * FeaturesUtil::declare_compatibility instead, passing the full plugin file path instead of the plugin name.
	 *
	 * @param string $feature_id Unique feature id.
	 * @param string $plugin_name Plugin name, in the form 'directory/file.php'.
	 * @param bool   $positive_compatibility True if the plugin declares being compatible with the feature, false if it declares being incompatible.
	 * @return bool True on success, false on error (feature doesn't exist or not inside the required hook).
	 * @throws \Exception A plugin attempted to declare itself as compatible and incompatible with a given feature at the same time.
	 */
	public function declare_compatibility( string $feature_id, string $plugin_name, bool $positive_compatibility = true ): bool {
		if ( ! $this->proxy->call_function( 'doing_action', 'before_woocommerce_init' ) ) {
			$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . __FUNCTION__;
			/* translators: 1: class::method 2: before_woocommerce_init */
			$this->proxy->call_function( 'wc_doing_it_wrong', $class_and_method, sprintf( __( '%1$s should be called inside the %2$s action.', 'woocommerce' ), $class_and_method, 'before_woocommerce_init' ), '7.0' );
			return false;
		}

		if ( ! $this->feature_exists( $feature_id ) ) {
			return false;
		}

		$plugin_name = str_replace( '\\', '/', $plugin_name );

		// Register compatibility by plugin.

		ArrayUtil::ensure_key_is_array( $this->compatibility_info_by_plugin, $plugin_name );

		$key          = $positive_compatibility ? 'compatible' : 'incompatible';
		$opposite_key = $positive_compatibility ? 'incompatible' : 'compatible';
		ArrayUtil::ensure_key_is_array( $this->compatibility_info_by_plugin[ $plugin_name ], $key );
		ArrayUtil::ensure_key_is_array( $this->compatibility_info_by_plugin[ $plugin_name ], $opposite_key );

		if ( in_array( $feature_id, $this->compatibility_info_by_plugin[ $plugin_name ][ $opposite_key ], true ) ) {
			throw new \Exception( "Plugin $plugin_name is trying to declare itself as $key with the '$feature_id' feature, but it already declared itself as $opposite_key" );
		}

		if ( ! in_array( $feature_id, $this->compatibility_info_by_plugin[ $plugin_name ][ $key ], true ) ) {
			$this->compatibility_info_by_plugin[ $plugin_name ][ $key ][] = $feature_id;
		}

		// Register compatibility by feature.

		$key = $positive_compatibility ? 'compatible' : 'incompatible';

		if ( ! in_array( $plugin_name, $this->compatibility_info_by_feature[ $feature_id ][ $key ], true ) ) {
			$this->compatibility_info_by_feature[ $feature_id ][ $key ][] = $plugin_name;
		}

		return true;
	}

	/**
	 * Check whether a feature exists with a given id.
	 *
	 * @param string $feature_id The feature id to check.
	 * @return bool True if the feature exists.
	 */
	private function feature_exists( string $feature_id ): bool {
		return isset( $this->features[ $feature_id ] );
	}

	/**
	 * Get the ids of the features that a certain plugin has declared compatibility for.
	 *
	 * This method can't be called before the 'woocommerce_init' hook is fired.
	 *
	 * @param string $plugin_name Plugin name, in the form 'directory/file.php'.
	 * @param bool   $enabled_features_only True to return only names of enabled plugins.
	 * @return array An array having a 'compatible' and an 'incompatible' key, each holding an array of feature ids.
	 */
	public function get_compatible_features_for_plugin( string $plugin_name, bool $enabled_features_only = false ) : array {
		$this->verify_did_woocommerce_init( __FUNCTION__ );

		$features = $this->features;
		if ( $enabled_features_only ) {
			$features = array_filter(
				$features,
				array( $this, 'feature_is_enabled' ),
				ARRAY_FILTER_USE_KEY
			);
		}

		if ( ! isset( $this->compatibility_info_by_plugin[ $plugin_name ] ) ) {
			return array(
				'compatible'   => array(),
				'incompatible' => array(),
				'uncertain'    => array_keys( $features ),
			);
		}

		$info                 = $this->compatibility_info_by_plugin[ $plugin_name ];
		$info['compatible']   = array_values( array_intersect( array_keys( $features ), $info['compatible'] ) );
		$info['incompatible'] = array_values( array_intersect( array_keys( $features ), $info['incompatible'] ) );
		$info['uncertain']    = array_values( array_diff( array_keys( $features ), $info['compatible'], $info['incompatible'] ) );

		return $info;
	}

	/**
	 * Get the names of the plugins that have been declared compatible or incompatible with a given feature.
	 *
	 * @param string $feature_id Feature id.
	 * @param bool   $active_only True to return only active plugins.
	 * @return array An array having a 'compatible' and an 'incompatible' key, each holding an array of plugin names.
	 */
	public function get_compatible_plugins_for_feature( string $feature_id, bool $active_only = false ) : array {
		$this->verify_did_woocommerce_init( __FUNCTION__ );

		$woo_aware_plugins = $this->plugin_util->get_woocommerce_aware_plugins( $active_only );
		if ( ! $this->feature_exists( $feature_id ) ) {
			return array(
				'compatible'   => array(),
				'incompatible' => array(),
				'uncertain'    => $woo_aware_plugins,
			);
		}

		$info              = $this->compatibility_info_by_feature[ $feature_id ];
		$info['uncertain'] = array_values( array_diff( $woo_aware_plugins, $info['compatible'], $info['incompatible'] ) );

		return $info;
	}

	/**
	 * Check if the 'woocommerce_init' has run or is running, do a 'wc_doing_it_wrong' if not.
	 *
	 * @param string|null $function Name of the invoking method, if not null, 'wc_doing_it_wrong' will be invoked if 'woocommerce_init' has not run and is not running.
	 * @return bool True if 'woocommerce_init' has run or is running, false otherwise.
	 */
	private function verify_did_woocommerce_init( string $function = null ): bool {
		if ( ! $this->proxy->call_function( 'did_action', 'woocommerce_init' ) &&
			! $this->proxy->call_function( 'doing_action', 'woocommerce_init' ) ) {
			if ( ! is_null( $function ) ) {
				$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . $function;
				/* translators: 1: class::method 2: plugins_loaded */
				$this->proxy->call_function( 'wc_doing_it_wrong', $class_and_method, sprintf( __( '%1$s should not be called before the %2$s action.', 'woocommerce' ), $class_and_method, 'woocommerce_init' ), '7.0' );
			}
			return false;
		}

		return true;
	}

	/**
	 * Get the name of the option that enables/disables a given feature.
	 * Note that it doesn't check if the feature actually exists.
	 *
	 * @param string $feature_id The id of the feature.
	 * @return string The option that enables or disables the feature.
	 */
	public function feature_enable_option_name( string $feature_id ): string {
		if ( 'analytics' === $feature_id ) {
			return Analytics::TOGGLE_OPTION_NAME;
		} elseif ( 'new_navigation' === $feature_id ) {
			return Init::TOGGLE_OPTION_NAME;
		}

		return "woocommerce_feature_${feature_id}_enabled";
	}

	/**
	 * Checks whether a feature id corresponds to a legacy feature
	 * (a feature that existed prior to the implementation of the features engine).
	 *
	 * @param string $feature_id The feature id to check.
	 * @return bool True if the id corresponds to a legacy feature.
	 */
	public function is_legacy_feature( string $feature_id ): bool {
		return in_array( $feature_id, $this->legacy_feature_ids, true );
	}

	/**
	 * Sets a flag indicating that it's allowed to enable features for which incompatible plugins are active
	 * from the WooCommerce feature settings page.
	 */
	public function allow_enabling_features_with_incompatible_plugins(): void {
		$this->force_allow_enabling_features = true;
	}

	/**
	 * Sets a flag indicating that it's allowed to activate plugins for which incompatible features are enabled
	 * from the WordPress plugins page.
	 */
	public function allow_activating_plugins_with_incompatible_features(): void {
		$this->force_allow_enabling_plugins = true;
	}

	/**
	 * Handler for the 'added_option' hook.
	 *
	 * It fires FEATURE_ENABLED_CHANGED_ACTION when a feature is enabled or disabled.
	 *
	 * @param string $option The option that has been created.
	 * @param mixed  $value The value of the option.
	 */
	private function process_added_option( string $option, $value ) {
		$this->process_updated_option( $option, false, $value );
	}

	/**
	 * Handler for the 'updated_option' hook.
	 *
	 * It fires FEATURE_ENABLED_CHANGED_ACTION when a feature is enabled or disabled.
	 *
	 * @param string $option The option that has been modified.
	 * @param mixed  $old_value The old value of the option.
	 * @param mixed  $value The new value of the option.
	 */
	private function process_updated_option( string $option, $old_value, $value ) {
		$matches = array();
		$success = preg_match( '/^woocommerce_feature_([a-zA-Z0-9_]+)_enabled$/', $option, $matches );

		if ( ! $success && Analytics::TOGGLE_OPTION_NAME !== $option && Init::TOGGLE_OPTION_NAME !== $option ) {
			return;
		}

		if ( $value === $old_value ) {
			return;
		}

		if ( Analytics::TOGGLE_OPTION_NAME === $option ) {
			$feature_id = 'analytics';
		} elseif ( Init::TOGGLE_OPTION_NAME === $option ) {
			$feature_id = 'new_navigation';
		} else {
			$feature_id = $matches[1];
		}

		/**
		 * Action triggered when a feature is enabled or disabled (the value of the corresponding setting option is changed).
		 *
		 * @param string $feature_id The id of the feature.
		 * @param bool $enabled True if the feature has been enabled, false if it has been disabled.
		 *
		 * @since 7.0.0
		 */
		do_action( self::FEATURE_ENABLED_CHANGED_ACTION, $feature_id, 'yes' === $value );
	}

	/**
	 * Handler for the 'woocommerce_get_sections_advanced' hook,
	 * it adds the "Features" section to the advanced settings page.
	 *
	 * @param array $sections The original sections array.
	 * @return array The updated sections array.
	 */
	private function add_features_section( $sections ) {
		if ( ! isset( $sections['features'] ) ) {
			$sections['features'] = __( 'Features', 'woocommerce' );
		}
		return $sections;
	}

	/**
	 * Handler for the 'woocommerce_get_settings_advanced' hook,
	 * it adds the settings UI for all the existing features.
	 *
	 * Note that the settings added via the 'woocommerce_settings_features' hook will be
	 * displayed in the non-experimental features section.
	 *
	 * @param array  $settings The existing settings for the corresponding settings section.
	 * @param string $current_section The section to get the settings for.
	 * @return array The updated settings array.
	 */
	private function add_feature_settings( $settings, $current_section ): array {
		if ( 'features' !== $current_section ) {
			return $settings;
		}

		// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
		/**
		 * Filter allowing WooCommerce Admin to be disabled.
		 *
		 * @param bool $disabled False.
		 */
		$admin_features_disabled = apply_filters( 'woocommerce_admin_disabled', false );
		// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment

		$feature_settings =
			array(
				array(
					'title' => __( 'Features', 'woocommerce' ),
					'type'  => 'title',
					'desc'  => __( 'Start using new features that are being progressively rolled out to improve the store management experience.', 'woocommerce' ),
					'id'    => 'features_options',
				),
			);

		$features = $this->get_features( true );

		$feature_ids              = array_keys( $features );
		$experimental_feature_ids = array_filter(
			$feature_ids,
			function( $feature_id ) use ( $features ) {
				return $features[ $feature_id ]['is_experimental'];
			}
		);
		$mature_feature_ids       = array_diff( $feature_ids, $experimental_feature_ids );
		$feature_ids              = array_merge( $mature_feature_ids, array( 'mature_features_end' ), $experimental_feature_ids );

		foreach ( $feature_ids as $id ) {
			if ( 'mature_features_end' === $id ) {
				// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
				/**
				 * Filter allowing to add additional settings to the WooCommerce Advanced - Features settings page.
				 *
				 * @param bool $disabled False.
				 */
				$additional_features = apply_filters( 'woocommerce_settings_features', $features );
				// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment

				$feature_settings = array_merge( $feature_settings, $additional_features );

				if ( ! empty( $experimental_feature_ids ) ) {
					$feature_settings[] = array(
						'type' => 'sectionend',
						'id'   => 'features_options',
					);

					$feature_settings[] = array(
						'title' => __( 'Experimental features', 'woocommerce' ),
						'type'  => 'title',
						'desc'  => __( 'These features are either experimental or incomplete, enable them at your own risk!', 'woocommerce' ),
						'id'    => 'experimental_features_options',
					);
				}
				continue;
			}

			$feature_settings[] = $this->get_setting_for_feature( $id, $features[ $id ], $admin_features_disabled );
		}

		$feature_settings[] = array(
			'type' => 'sectionend',
			'id'   => empty( $experimental_feature_ids ) ? 'features_options' : 'experimental_features_options',
		);

		return $feature_settings;
	}

	/**
	 * Get the parameters to display the setting enable/disable UI for a given feature.
	 *
	 * @param string $feature_id The feature id.
	 * @param array  $feature The feature parameters, as returned by get_features.
	 * @param bool   $admin_features_disabled True if admin features have been disabled via 'woocommerce_admin_disabled' filter.
	 * @return array The parameters to add to the settings array.
	 */
	private function get_setting_for_feature( string $feature_id, array $feature, bool $admin_features_disabled ): array {
		$description = $feature['description'];
		$disabled    = false;
		$desc_tip    = '';

		if ( ( 'analytics' === $feature_id || 'new_navigation' === $feature_id ) && $admin_features_disabled ) {
			$disabled = true;
			$desc_tip = __( 'WooCommerce Admin has been disabled', 'woocommerce' );
		} elseif ( 'new_navigation' === $feature_id ) {
			$needs_update = version_compare( get_bloginfo( 'version' ), '5.6', '<' );
			if ( $needs_update && current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
				$update_text = sprintf(
				// translators: 1: line break tag, 2: open link to WordPress update link, 3: close link tag.
					__( '%1$s %2$sUpdate WordPress to enable the new navigation%3$s', 'woocommerce' ),
					'<br/>',
					'<a href="' . self_admin_url( 'update-core.php' ) . '" target="_blank">',
					'</a>'
				);
				$description .= $update_text;
				$disabled     = true;
			}
		}

		if ( ! $this->is_legacy_feature( $feature_id ) && ! $disabled && $this->verify_did_woocommerce_init() ) {
			$plugin_info_for_feature = $this->get_compatible_plugins_for_feature( $feature_id, true );
			$incompatibles           = array_merge( $plugin_info_for_feature['incompatible'], $plugin_info_for_feature['uncertain'] );
			$incompatibles           = array_filter( $incompatibles, 'is_plugin_active' );
			$incompatible_count      = count( $incompatibles );
			if ( $incompatible_count > 0 ) {
				if ( 1 === $incompatible_count ) {
					/* translators: %s = printable plugin name */
					$desc_tip = sprintf( __( "⚠ This feature shouldn't be enabled, the %s plugin is active and isn't compatible with it.", 'woocommerce' ), $this->plugin_util->get_plugin_name( $incompatibles[0] ) );
				} elseif ( 2 === $incompatible_count ) {
					/* translators: %1\$s, %2\$s = printable plugin names */
					$desc_tip = sprintf(
						__( "⚠ This feature shouldn't be enabled: the %1\$s and %2\$s plugins are active and aren't compatible with it.", 'woocommerce' ),
						$this->plugin_util->get_plugin_name( $incompatibles[0] ),
						$this->plugin_util->get_plugin_name( $incompatibles[1] )
					);
				} else {
					/* translators: %1\$s, %2\$s = printable plugin names, %3\$d = plugins count */
					$desc_tip = sprintf(
						_n(
							"⚠ This feature shouldn't be enabled: %1\$s, %2\$s and %3\$d more active plugin isn't compatible with it",
							"⚠ This feature shouldn't be enabled: the %1\$s and %2\$s plugins are active and aren't compatible with it. There are %3\$d other incompatible plugins.",
							$incompatible_count - 2,
							'woocommerce'
						),
						$this->plugin_util->get_plugin_name( $incompatibles[0] ),
						$this->plugin_util->get_plugin_name( $incompatibles[1] ),
						$incompatible_count - 2
					);
				}

				$incompatible_plugins_url = add_query_arg(
					array(
						'plugin_status' => 'incompatible_with_feature',
						'feature_id'    => $feature_id,
					),
					admin_url( 'plugins.php' )
				);
				/* translators: %s = URL of the plugins page */
				$extra_desc_tip = sprintf( __( " <a href='%s'>Manage incompatible plugins</a>", 'woocommerce' ), $incompatible_plugins_url );

				$desc_tip .= $extra_desc_tip;

				$disabled = ! $this->feature_is_enabled( $feature_id );
			}
		}

		/**
		 * Filter to customize the description tip that appears under the description of each feature in the features settings page.
		 *
		 * @since 7.1.0
		 *
		 * @param string $desc_tip The original description tip.
		 * @param string $feature_id The id of the feature for which the description tip is being customized.
		 * @param bool $disabled True if the UI currently prevents changing the enable/disable status of the feature.
		 * @return string The new description tip to use.
		 */
		$desc_tip = apply_filters( 'woocommerce_feature_description_tip', $desc_tip, $feature_id, $disabled );

		return array(
			'title'    => $feature['name'],
			'desc'     => $description,
			'type'     => 'checkbox',
			'id'       => $this->feature_enable_option_name( $feature_id ),
			'disabled' => $disabled && ! $this->force_allow_enabling_features,
			'desc_tip' => $desc_tip,
			'default'  => $this->feature_is_enabled_by_default( $feature_id ) ? 'yes' : 'no',
		);
	}

	/**
	 * Handle the plugin deactivation hook.
	 *
	 * @param string $plugin_name Name of the plugin that has been deactivated.
	 */
	private function handle_plugin_deactivation( $plugin_name ): void {
		unset( $this->compatibility_info_by_plugin[ $plugin_name ] );

		foreach ( array_keys( $this->compatibility_info_by_feature ) as $feature ) {
			$compatibles = $this->compatibility_info_by_feature[ $feature ]['compatible'];
			$this->compatibility_info_by_feature[ $feature ]['compatible'] = array_diff( $compatibles, array( $plugin_name ) );

			$incompatibles = $this->compatibility_info_by_feature[ $feature ]['incompatible'];
			$this->compatibility_info_by_feature[ $feature ]['incompatible'] = array_diff( $incompatibles, array( $plugin_name ) );

		}
	}

	/**
	 * Handler for the all_plugins filter.
	 *
	 * Returns the list of plugins incompatible with a given plugin
	 * if we are in the plugins page and the query string of the current request
	 * looks like '?plugin_status=incompatible_with_feature&feature_id=<feature id>'.
	 *
	 * @param array $list The original list of plugins.
	 */
	private function filter_plugins_list( $list ): array {
		if ( ! $this->verify_did_woocommerce_init() ) {
			return $list;
		}

		// phpcs:disable WordPress.Security.NonceVerification
		if ( ! function_exists( 'get_current_screen' ) || get_current_screen() && 'plugins' !== get_current_screen()->id || 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) {
			return $list;
		}

		$feature_id = ArrayUtil::get_value_or_default( $_GET, 'feature_id', 'all' );
		if ( 'all' !== $feature_id && ! $this->feature_exists( $feature_id ) ) {
			return $list;
		}

		$incompatibles = array();

		// phpcs:enable WordPress.Security.NonceVerification
		foreach ( array_keys( $list ) as $plugin_name ) {
			if ( ! $this->plugin_util->is_woocommerce_aware_plugin( $plugin_name ) || ! $this->proxy->call_function( 'is_plugin_active', $plugin_name ) ) {
				continue;
			}

			$compatibility     = $this->get_compatible_features_for_plugin( $plugin_name );
			$incompatible_with = array_diff(
				array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ),
				$this->legacy_feature_ids
			);

			if ( ( 'all' === $feature_id && ! empty( $incompatible_with ) ) || in_array( $feature_id, $incompatible_with, true ) ) {
				$incompatibles[] = $plugin_name;
			}
		}

		return array_intersect_key( $list, array_flip( $incompatibles ) );
	}

	/**
	 * Handler for the admin_notices action.
	 */
	private function display_notices_in_plugins_page(): void {
		if ( ! $this->verify_did_woocommerce_init() ) {
			return;
		}

		$feature_filter_description_shown = $this->maybe_display_current_feature_filter_description();
		if ( ! $feature_filter_description_shown ) {
			$this->maybe_display_feature_incompatibility_warning();
		}
	}

	/**
	 * Shows a warning when there are any incompatibility between active plugins and enabled features.
	 * The warning is shown in on any admin screen except the plugins screen itself, since
	 * there's already a "You are viewing
	 */
	private function maybe_display_feature_incompatibility_warning(): void {
		$incompatible_plugins = false;

		foreach ( $this->plugin_util->get_woocommerce_aware_plugins( true ) as $plugin ) {
			$compatibility     = $this->get_compatible_features_for_plugin( $plugin, true );
			$incompatible_with = array_diff(
				array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ),
				$this->legacy_feature_ids
			);

			if ( $incompatible_with ) {
				$incompatible_plugins = true;
				break;
			}
		}

		if ( ! $incompatible_plugins ) {
			return;
		}

		$message = str_replace(
			'<a>',
			'<a href="' . esc_url( add_query_arg( array( 'plugin_status' => 'incompatible_with_feature' ), admin_url( 'plugins.php' ) ) ) . '">',
			__( 'WooCommerce has detected that some of your active plugins are incompatible with currently enabled WooCommerce features. Please <a>review the details</a>.', 'woocommerce' )
		);

		// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
		?>
		<div class="notice notice-error">
		<p><?php echo $message; ?></p>
		</div>
		<?php
		// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Shows a "You are viewing the plugins that are incompatible with the X feature"
	 * if we are in the plugins page and the query string of the current request
	 * looks like '?plugin_status=incompatible_with_feature&feature_id=<feature id>'.
	 */
	private function maybe_display_current_feature_filter_description(): bool {
		if ( 'plugins' !== get_current_screen()->id ) {
			return false;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
		$plugin_status = $_GET['plugin_status'] ?? '';
		$feature_id    = $_GET['feature_id'] ?? '';
		// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput

		if ( 'incompatible_with_feature' !== $plugin_status ) {
			return false;
		}

		$feature_id = ( '' === $feature_id ) ? 'all' : $feature_id;

		if ( 'all' !== $feature_id && ! $this->feature_exists( $feature_id ) ) {
			return false;
		}

		// phpcs:enable WordPress.Security.NonceVerification
		$plugins_page_url  = admin_url( 'plugins.php' );
		$features_page_url = $this->get_features_page_url();

		$message =
			'all' === $feature_id
			? __( 'You are viewing active plugins that are incompatible with currently enabled WooCommerce features.', 'woocommerce' )
			: sprintf(
				/* translators: %s is a feature name. */
				__( "You are viewing the active plugins that are incompatible with the '%s' feature.", 'woocommerce' ),
				$this->features[ $feature_id ]['name']
			);

		$message .= '<br />';
		$message .= sprintf(
			__( "<a href='%1\$s'>View all plugins</a> - <a href='%2\$s'>Manage WooCommerce features</a>", 'woocommerce' ),
			$plugins_page_url,
			$features_page_url
		);

		// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
		?>
		<div class="notice notice-info">
			<p><?php echo $message; ?></p>
		</div>
		<?php
		// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped

		return true;
	}

	/**
	 * Handler for the 'after_plugin_row' action.
	 * Displays a "This plugin is incompatible with X features" notice if necessary.
	 *
	 * @param string $plugin_file The id of the plugin for which a row has been rendered in the plugins page.
	 * @param array  $plugin_data Plugin data, as returned by 'get_plugins'.
	 */
	private function handle_plugin_list_rows( $plugin_file, $plugin_data ) {
		global $wp_list_table;

		if ( 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			return;
		}

		if ( is_null( $wp_list_table ) || ! $this->plugin_util->is_woocommerce_aware_plugin( $plugin_data ) ) {
			return;
		}

		if ( ! $this->proxy->call_function( 'is_plugin_active', $plugin_file ) ) {
			return;
		}

		$feature_compatibility_info = $this->get_compatible_features_for_plugin( $plugin_file, true );
		$incompatible_features      = array_merge( $feature_compatibility_info['incompatible'], $feature_compatibility_info['uncertain'] );
		$incompatible_features      = array_values(
			array_filter(
				$incompatible_features,
				function( $feature_id ) {
					return ! $this->is_legacy_feature( $feature_id );
				}
			)
		);

		$incompatible_features_count = count( $incompatible_features );
		if ( $incompatible_features_count > 0 ) {
			$columns_count      = $wp_list_table->get_column_count();
			$is_active          = true; // For now we are showing active plugins in the "Incompatible with..." view.
			$is_active_class    = $is_active ? 'active' : 'inactive';
			$is_active_td_style = $is_active ? " style='border-left: 4px solid #72aee6;'" : '';

			if ( 1 === $incompatible_features_count ) {
				$message = sprintf(
					/* translators: %s = printable plugin name */
					__( "⚠ This plugin is incompatible with the enabled WooCommerce feature '%s', it shouldn't be activated.", 'woocommerce' ),
					$this->features[ $incompatible_features[0] ]['name']
				);
			} elseif ( 2 === $incompatible_features_count ) {
				/* translators: %1\$s, %2\$s = printable plugin names */
				$message = sprintf(
					__( "⚠ This plugin is incompatible with the enabled WooCommerce features '%1\$s' and '%2\$s', it shouldn't be activated.", 'woocommerce' ),
					$this->features[ $incompatible_features[0] ]['name'],
					$this->features[ $incompatible_features[1] ]['name']
				);
			} else {
				/* translators: %1\$s, %2\$s = printable plugin names, %3\$d = plugins count */
				$message = sprintf(
					__( "⚠ This plugin is incompatible with the enabled WooCommerce features '%1\$s', '%2\$s' and %3\$d more, it shouldn't be activated.", 'woocommerce' ),
					$this->features[ $incompatible_features[0] ]['name'],
					$this->features[ $incompatible_features[1] ]['name'],
					$incompatible_features_count - 2
				);
			}
			$features_page_url       = $this->get_features_page_url();
			$manage_features_message = __( 'Manage WooCommerce features', 'woocommerce' );

			// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
			?>
			<tr class='plugin-update-tr update <?php echo $is_active_class; ?>' data-plugin='<?php echo $plugin_file; ?>' data-plugin-row-type='feature-incomp-warn'>
				<td colspan='<?php echo $columns_count; ?>' class='plugin-update'<?php echo $is_active_td_style; ?>>
					<div class='notice inline notice-warning notice-alt'>
						<p>
							<?php echo $message; ?>
							<a href="<?php echo $features_page_url; ?>"><?php echo $manage_features_message; ?></a>
						</p>
					</div>
				</td>
			</tr>
			<?php
			// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
		}
	}

	/**
	 * Get the URL of the features settings page.
	 *
	 * @return string
	 */
	private function get_features_page_url(): string {
		return admin_url( 'admin.php?page=wc-settings&tab=advanced&section=features' );
	}

	/**
	 * Fix for the HTML of the plugins list when there are feature-plugin incompatibility warnings.
	 *
	 * WordPress renders the plugin information rows in the plugins page in <tr> elements as follows:
	 *
	 * - If the plugin needs update, the <tr> will have an "update" class. This will prevent the lower
	 *   border line to be drawn. Later an additional <tr> with an "update available" warning will be rendered,
	 *   it will have a "plugin-update-tr" class which will draw the missing lower border line.
	 * - Otherwise, the <tr> will be already drawn with the lower border line.
	 *
	 * This is a problem for our rendering of the "plugin is incompatible with X features" warning:
	 *
	 * - If the plugin info <tr> has "update", our <tr> will render nicely right after it; but then
	 *   our own "plugin-update-tr" class will draw an additional line before the "needs update" warning.
	 * - If not, the plugin info <tr> will render its lower border line right before our compatibility info <tr>.
	 *
	 * This small script fixes this by adding the "update" class to the plugin info <tr> if it doesn't have it
	 * (so no extra line before our <tr>), or removing 'plugin-update-tr' from our <tr> otherwise
	 * (and then some extra manual tweaking of margins is needed).
	 *
	 * @param string $current_screen The current screen object.
	 */
	private function enqueue_script_to_fix_plugin_list_html( $current_screen ): void {
		if ( 'plugins' !== $current_screen->id ) {
			return;
		}

		wc_enqueue_js(
			"
	    const warningRows = document.querySelectorAll('tr[data-plugin-row-type=\"feature-incomp-warn\"]');
	    for(const warningRow of warningRows) {
	    	const pluginName = warningRow.getAttribute('data-plugin');
			const pluginInfoRow = document.querySelector('tr.active[data-plugin=\"' + pluginName + '\"]:not(.plugin-update-tr), tr.inactive[data-plugin=\"' + pluginName + '\"]:not(.plugin-update-tr)');
			if(pluginInfoRow.classList.contains('update')) {
				warningRow.classList.remove('plugin-update-tr');
				warningRow.querySelector('.notice').style.margin = '5px 10px 15px 30px';
			}
			else {
				pluginInfoRow.classList.add('update');
			}
	    }
		"
		);
	}

	/**
	 * Handler for the 'views_plugins' hook that shows the links to the different views in the plugins page.
	 * If we come from a "Manage incompatible plugins" in the features page we'll show just two views:
	 * "All" (so that it's easy to go back to a known state) and "Incompatible with X".
	 * We'll skip the rest of the views since the counts are wrong anyway, as we are modifying
	 * the plugins list via the 'all_plugins' filter.
	 *
	 * @param array $views An array of view ids => view links.
	 * @return string[] The actual views array to use.
	 */
	private function handle_plugins_page_views_list( $views ): array {
		// phpcs:disable WordPress.Security.NonceVerification
		if ( 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) {
			return $views;
		}

		$feature_id = ArrayUtil::get_value_or_default( $_GET, 'feature_id', 'all' );
		if ( 'all' !== $feature_id && ! $this->feature_exists( $feature_id ) ) {
			return $views;
		}
		// phpcs:enable WordPress.Security.NonceVerification

		$all_items = get_plugins();

		$incompatible_plugins_count = count( $this->filter_plugins_list( $all_items ) );
		$incompatible_text          =
			'all' === $feature_id
			? __( 'Incompatible with WooCommerce features', 'woocommerce' )
			/* translators: %s = name of a WooCommerce feature */
			: sprintf( __( "Incompatible with '%s'", 'woocommerce' ), $this->features[ $feature_id ]['name'] );
		$incompatible_link = "<a href='plugins.php?plugin_status=incompatible_with_feature&feature_id={$feature_id}' class='current' aria-current='page'>{$incompatible_text} <span class='count'>({$incompatible_plugins_count})</span></a>";

		$all_plugins_count = count( $all_items );
		$all_text          = __( 'All', 'woocommerce' );
		$all_link          = "<a href='plugins.php?plugin_status=all'>{$all_text} <span class='count'>({$all_plugins_count})</span></a>";

		return array(
			'all'                       => $all_link,
			'incompatible_with_feature' => $incompatible_link,
		);
	}
}
Internal/Admin/Coupons.php000064400000006153152333011040011504 0ustar00<?php
/**
 * WooCommerce Marketing > Coupons.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Internal\Admin\Notes\CouponPageMoved;
use Automattic\WooCommerce\Admin\PageController;

/**
 * Contains backend logic for the Coupons feature.
 */
class Coupons {

	use CouponsMovedTrait;

	/**
	 * Class instance.
	 *
	 * @var Coupons instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}

		// If the main marketing feature is disabled, don't modify coupon behavior.
		if ( ! Features::is_enabled( 'marketing' ) ) {
			return;
		}

		// Only support coupon modifications if coupons are enabled.
		if ( ! wc_coupons_enabled() ) {
			return;
		}

		( new CouponPageMoved() )->init();

		add_action( 'admin_enqueue_scripts', array( $this, 'maybe_add_marketing_coupon_script' ) );
		add_action( 'woocommerce_register_post_type_shop_coupon', array( $this, 'move_coupons' ) );
		add_action( 'admin_head', array( $this, 'fix_coupon_menu_highlight' ), 99 );
		add_action( 'admin_menu', array( $this, 'maybe_add_coupon_menu_redirect' ) );
	}

	/**
	 * Maybe add menu item back in original spot to help people transition
	 */
	public function maybe_add_coupon_menu_redirect() {
		if ( ! $this->should_display_legacy_menu() ) {
			return;
		}

		add_submenu_page(
			'woocommerce',
			__( 'Coupons', 'woocommerce' ),
			__( 'Coupons', 'woocommerce' ),
			'manage_options',
			'coupons-moved',
			[ $this, 'coupon_menu_moved' ]
		);
	}

	/**
	 * Call back for transition menu item
	 */
	public function coupon_menu_moved() {
		wp_safe_redirect( $this->get_legacy_coupon_url(), 301 );
		exit();
	}

	/**
	 * Modify registered post type shop_coupon
	 *
	 * @param array $args Array of post type parameters.
	 *
	 * @return array the filtered parameters.
	 */
	public function move_coupons( $args ) {
		$args['show_in_menu'] = current_user_can( 'manage_woocommerce' ) ? 'woocommerce-marketing' : true;
		return $args;
	}

	/**
	 * Undo WC modifications to $parent_file for 'shop_coupon'
	 */
	public function fix_coupon_menu_highlight() {
		global $parent_file, $post_type;

		if ( $post_type === 'shop_coupon' ) {
			$parent_file = 'woocommerce-marketing'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
		}
	}

	/**
	 * Maybe add our wc-admin coupon scripts if viewing coupon pages
	 */
	public function maybe_add_marketing_coupon_script() {
		$curent_screen = PageController::get_instance()->get_current_page();
		if ( ! isset( $curent_screen['id'] ) || $curent_screen['id'] !== 'woocommerce-coupons' ) {
			return;
		}

		$rtl = is_rtl() ? '-rtl' : '';

		wp_enqueue_style(
			'wc-admin-marketing-coupons',
			WCAdminAssets::get_url( "marketing-coupons/style{$rtl}", 'css' ),
			array(),
			WCAdminAssets::get_file_version( 'css' )
		);

		WCAdminAssets::register_script( 'wp-admin-scripts', 'marketing-coupons', true );
	}
}
Internal/Admin/SystemStatusReport.php000064400000013537152333011040013746 0ustar00<?php
/**
 * Add additional system status report sections.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\Notes\Notes;
defined( 'ABSPATH' ) || exit;

/**
 * SystemStatusReport class.
 */
class SystemStatusReport {
	/**
	 * Class instance.
	 *
	 * @var SystemStatus instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		add_action( 'woocommerce_system_status_report', array( $this, 'system_status_report' ) );
	}

	/**
	 * Hooks extra necessary sections into the system status report template
	 */
	public function system_status_report() {
		?>
			<table class="wc_status_table widefat" cellspacing="0">
				<thead>
				<tr>
					<th colspan="5" data-export-label="Admin">
						<h2>
							<?php esc_html_e( 'Admin', 'woocommerce' ); ?><?php echo wc_help_tip( esc_html__( 'This section shows details of WC Admin.', 'woocommerce' ) ); ?>
						</h2>
					</th>
				</tr>
				</thead>
				<tbody>
					<?php
						$this->render_features();
						$this->render_daily_cron();
						$this->render_options();
						$this->render_notes();
						$this->render_onboarding_state();
					?>
				</tbody>
			</table>
		<?php
	}

	/**
	 * Render features rows.
	 */
	public function render_features() {
		/**
		 * Filter the admin feature configs.
		 *
		 * @since 6.5.0
		 */
		$features          = apply_filters( 'woocommerce_admin_get_feature_config', wc_admin_get_feature_config() );
		$enabled_features  = array_filter( $features );
		$disabled_features = array_filter(
			$features,
			function( $feature ) {
				return empty( $feature );
			}
		);

		?>
			<tr>
				<td data-export-label="Enabled Features">
					<?php esc_html_e( 'Enabled Features', 'woocommerce' ); ?>:
				</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'Which features are enabled?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td>
					<?php
						echo esc_html( implode( ', ', array_keys( $enabled_features ) ) )
					?>
				</td>
			</tr>

			<tr>
				<td data-export-label="Disabled Features">
					<?php esc_html_e( 'Disabled Features', 'woocommerce' ); ?>:
				</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'Which features are disabled?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td>
					<?php
						echo esc_html( implode( ', ', array_keys( $disabled_features ) ) )
					?>
				</td>
			</tr>
		<?php
	}


	/**
	 * Render daily cron row.
	 */
	public function render_daily_cron() {
		$next_daily_cron = wp_next_scheduled( 'wc_admin_daily' );
		?>
			<tr>
				<td data-export-label="Daily Cron">
					<?php esc_html_e( 'Daily Cron', 'woocommerce' ); ?>:
				</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'Is the daily cron job active, when does it next run?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td>
					<?php
					if ( empty( $next_daily_cron ) ) {
						echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Not scheduled', 'woocommerce' ) . '</mark>';
					} else {
						echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> Next scheduled: ' . esc_html( date_i18n( 'Y-m-d H:i:s P', $next_daily_cron ) ) . '</mark>';
					}
					?>
				</td>
			</tr>
		<?php
	}

	/**
	 * Render option row.
	 */
	public function render_options() {
		$woocommerce_admin_install_timestamp = get_option( 'woocommerce_admin_install_timestamp' );

		$all_options_expected = is_numeric( $woocommerce_admin_install_timestamp )
			&& 0 < (int) $woocommerce_admin_install_timestamp
			&& is_array( get_option( 'woocommerce_onboarding_profile', array() ) );

		?>
			<tr>
				<td data-export-label="Options">
					<?php esc_html_e( 'Options', 'woocommerce' ); ?>:
				</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'Do the important options return expected values?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td>
					<?php
					if ( $all_options_expected ) {
						echo '<mark class="yes"><span class="dashicons dashicons-yes"></mark>';
					} else {
						echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Not all expected', 'woocommerce' ) . '</mark>';
					}
					?>
				</td>
			</tr>
		<?php
	}

	/**
	 * Render the notes row.
	 */
	public function render_notes() {
		$notes_count = Notes::get_notes_count();

		?>
			<tr>
				<td data-export-label="Notes">
					<?php esc_html_e( 'Notes', 'woocommerce' ); ?>:
				</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'How many notes in the database?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td>
					<?php
						echo esc_html( $notes_count )
					?>
				</td>
			</tr>
		<?php
	}

	/**
	 * Render the onboarding state row.
	 */
	public function render_onboarding_state() {
		$onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() );
		$onboarding_state   = '-';

		if ( isset( $onboarding_profile['skipped'] ) && $onboarding_profile['skipped'] ) {
			$onboarding_state = 'skipped';
		}

		if ( isset( $onboarding_profile['completed'] ) && $onboarding_profile['completed'] ) {
			$onboarding_state = 'completed';
		}

		?>
			<tr>
				<td data-export-label="Onboarding">
					<?php esc_html_e( 'Onboarding', 'woocommerce' ); ?>:
				</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'Was onboarding completed or skipped?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td>
					<?php
						echo esc_html( $onboarding_state )
					?>
				</td>
			</tr>
		<?php
	}

}
Internal/Admin/CustomerEffortScoreTracks.php000064400000036140152333011040015170 0ustar00<?php
/**
 * WooCommerce Customer effort score tracks
 *
 * @package WooCommerce\Admin\Features
 */

namespace Automattic\WooCommerce\Internal\Admin;

defined( 'ABSPATH' ) || exit;

/**
 * Triggers customer effort score on several different actions.
 */
class CustomerEffortScoreTracks {
	/**
	 * Option name for the CES Tracks queue.
	 */
	const CES_TRACKS_QUEUE_OPTION_NAME = 'woocommerce_ces_tracks_queue';

	/**
	 * Option name for the clear CES Tracks queue for page.
	 */
	const CLEAR_CES_TRACKS_QUEUE_FOR_PAGE_OPTION_NAME =
		'woocommerce_clear_ces_tracks_queue_for_page';

	/**
	 * Option name for the set of actions that have been shown.
	 */
	const SHOWN_FOR_ACTIONS_OPTION_NAME = 'woocommerce_ces_shown_for_actions';

	/**
	 * Action name for product add/publish.
	 */
	const PRODUCT_ADD_PUBLISH_ACTION_NAME = 'product_add_publish';

	/**
	 * Action name for product update.
	 */
	const PRODUCT_UPDATE_ACTION_NAME = 'product_update';

	/**
	 * Action name for shop order update.
	 */
	const SHOP_ORDER_UPDATE_ACTION_NAME = 'shop_order_update';

	/**
	 * Action name for settings change.
	 */
	const SETTINGS_CHANGE_ACTION_NAME = 'settings_change';

	/**
	 * Action name for add product categories.
	 */
	const ADD_PRODUCT_CATEGORIES_ACTION_NAME = 'add_product_categories';

	/**
	 * Action name for add product tags.
	 */
	const ADD_PRODUCT_TAGS_ACTION_NAME = 'add_product_tags';

	/*
	 * Action name for add product attributes.
	 */
	const ADD_PRODUCT_ATTRIBUTES_ACTION_NAME = 'add_product_attributes';

	/**
	 * Action name for import products.
	 */
	const IMPORT_PRODUCTS_ACTION_NAME = 'import_products';

	/**
	 * Action name for search.
	 */
	const SEARCH_ACTION_NAME = 'ces_search';

	/**
	 * Label for the snackbar that appears when a user submits the survey.
	 *
	 * @var string
	 */
	private $onsubmit_label;

	/**
	 * Constructor. Sets up filters to hook into WooCommerce.
	 */
	public function __construct() {
		$this->enable_survey_enqueing_if_tracking_is_enabled();
	}

	/**
	 * Add actions that require woocommerce_allow_tracking.
	 */
	private function enable_survey_enqueing_if_tracking_is_enabled() {
		// Only hook up the action handlers if in wp-admin.
		if ( ! is_admin() ) {
			return;
		}

		// Do not hook up the action handlers if a mobile device is used.
		if ( wp_is_mobile() ) {
			return;
		}

		// Only enqueue a survey if tracking is allowed.
		$allow_tracking = 'yes' === get_option( 'woocommerce_allow_tracking', 'no' );
		if ( ! $allow_tracking ) {
			return;
		}

		add_action( 'admin_init', array( $this, 'maybe_clear_ces_tracks_queue' ) );
		add_action( 'woocommerce_update_options', array( $this, 'run_on_update_options' ), 10, 3 );
		add_action( 'product_cat_add_form', array( $this, 'add_script_track_product_categories' ), 10, 3 );
		add_action( 'product_tag_add_form', array( $this, 'add_script_track_product_tags' ), 10, 3 );
		add_action( 'woocommerce_attribute_added', array( $this, 'run_on_add_product_attributes' ), 10, 3 );
		add_action( 'load-edit.php', array( $this, 'run_on_load_edit_php' ), 10, 3 );
		add_action( 'product_page_product_importer', array( $this, 'run_on_product_import' ), 10, 3 );
		// Only hook up the transition_post_status action handler
		// if on the edit page.
		global $pagenow;
		if ( 'post.php' === $pagenow ) {
			add_action(
				'transition_post_status',
				array(
					$this,
					'run_on_transition_post_status',
				),
				10,
				3
			);
		}
		$this->onsubmit_label = __( 'Thank you for your feedback!', 'woocommerce' );
	}

	/**
	 * Returns a generated script for tracking tags added on edit-tags.php page.
	 * CES survey is triggered via direct access to wc/customer-effort-score store
	 * via wp.data.dispatch method.
	 *
	 * Due to lack of options to directly hook ourselves into the ajax post request
	 * initiated by edit-tags.php page, we infer a successful request by observing
	 * an increase of the number of rows in tags table
	 *
	 * @param string $action Action name for the survey.
	 * @param string $label Label for the snackbar.
	 *
	 * @return string Generated JavaScript to append to page.
	 */
	private function get_script_track_edit_php( $action, $label ) {
		return sprintf(
			"(function( $ ) {
				'use strict';
				// Hook on submit button and sets a 500ms interval function
				// to determine successful add tag or otherwise.
				$('#addtag #submit').on( 'click', function() {
					const initialCount = $('.tags tbody > tr').length;
					const interval = setInterval( function() {
						if ( $('.tags tbody > tr').length > initialCount ) {
							// New tag detected.
							clearInterval( interval );
							wp.data.dispatch('wc/customer-effort-score').addCesSurvey( '%s', '%s', window.pagenow, window.adminpage, '%s' );
						} else {
							// Form is no longer loading, most likely failed.
							if ( $( '#addtag .submit .spinner.is-active' ).length < 1 ) {
								clearInterval( interval );
							}
						}
					}, 500 );
				});
			})( jQuery );",
			esc_js( $action ),
			esc_js( $label ),
			esc_js( $this->onsubmit_label )
		);
	}

	/**
	 * Get the current published product count.
	 *
	 * @return integer The current published product count.
	 */
	private function get_product_count() {
		$query         = new \WC_Product_Query(
			array(
				'limit'    => 1,
				'paginate' => true,
				'return'   => 'ids',
				'status'   => array( 'publish' ),
			)
		);
		$products      = $query->get_products();
		$product_count = intval( $products->total );

		return $product_count;
	}

	/**
	 * Get the current shop order count.
	 *
	 * @return integer The current shop order count.
	 */
	private function get_shop_order_count() {
		$query            = new \WC_Order_Query(
			array(
				'limit'    => 1,
				'paginate' => true,
				'return'   => 'ids',
			)
		);
		$shop_orders      = $query->get_orders();
		$shop_order_count = intval( $shop_orders->total );

		return $shop_order_count;
	}

	/**
	 * Return whether the action has already been shown.
	 *
	 * @param string $action The action to check.
	 *
	 * @return bool Whether the action has already been shown.
	 */
	private function has_been_shown( $action ) {
		$shown_for_features = get_option( self::SHOWN_FOR_ACTIONS_OPTION_NAME, array() );
		$has_been_shown     = in_array( $action, $shown_for_features, true );

		return $has_been_shown;
	}

	/**
	 * Enqueue the item to the CES tracks queue.
	 *
	 * @param array $item The item to enqueue.
	 */
	private function enqueue_to_ces_tracks( $item ) {
		$queue = get_option(
			self::CES_TRACKS_QUEUE_OPTION_NAME,
			array()
		);

		$has_duplicate = array_filter(
			$queue,
			function ( $queue_item ) use ( $item ) {
				return $queue_item['action'] === $item['action'];
			}
		);
		if ( $has_duplicate ) {
			return;
		}

		$queue[] = $item;

		update_option(
			self::CES_TRACKS_QUEUE_OPTION_NAME,
			$queue
		);
	}

	/**
	 * Enqueue the CES survey on using search dynamically.
	 *
	 * @param string $search_area Search area such as "product" or "shop_order".
	 * @param string $page_now Value of window.pagenow.
	 * @param string $admin_page Value of window.adminpage.
	 */
	public function enqueue_ces_survey_for_search( $search_area, $page_now, $admin_page ) {
		if ( $this->has_been_shown( self::SEARCH_ACTION_NAME ) ) {
			return;
		}

		$this->enqueue_to_ces_tracks(
			array(
				'action'         => self::SEARCH_ACTION_NAME,
				'label'          => __(
					'How easy was it to use search?',
					'woocommerce'
				),
				'onsubmit_label' => $this->onsubmit_label,
				'pagenow'        => $page_now,
				'adminpage'      => $admin_page,
				'props'          => (object) array(
					'search_area' => $search_area,
				),
			)
		);
	}

	/**
	 * Hook into the post status lifecycle, to detect relevant user actions
	 * that we want to survey about.
	 *
	 * @param string $new_status The new status.
	 * @param string $old_status The old status.
	 * @param Post   $post The post.
	 */
	public function run_on_transition_post_status(
		$new_status,
		$old_status,
		$post
	) {
		if ( 'product' === $post->post_type ) {
			$this->maybe_enqueue_ces_survey_for_product( $new_status, $old_status );
		} elseif ( 'shop_order' === $post->post_type ) {
			$this->enqueue_ces_survey_for_edited_shop_order();
		}
	}

	/**
	 * Maybe enqueue the CES survey, if product is being added or edited.
	 *
	 * @param string $new_status The new status.
	 * @param string $old_status The old status.
	 */
	private function maybe_enqueue_ces_survey_for_product(
		$new_status,
		$old_status
	) {
		if ( 'publish' !== $new_status ) {
			return;
		}

		if ( 'publish' !== $old_status ) {
			$this->enqueue_ces_survey_for_new_product();
		} else {
			$this->enqueue_ces_survey_for_edited_product();
		}
	}

	/**
	 * Enqueue the CES survey trigger for a new product.
	 */
	private function enqueue_ces_survey_for_new_product() {
		if ( $this->has_been_shown( self::PRODUCT_ADD_PUBLISH_ACTION_NAME ) ) {
			return;
		}

		$this->enqueue_to_ces_tracks(
			array(
				'action'         => self::PRODUCT_ADD_PUBLISH_ACTION_NAME,
				'label'          => __(
					'How easy was it to add a product?',
					'woocommerce'
				),
				'onsubmit_label' => $this->onsubmit_label,
				'pagenow'        => 'product',
				'adminpage'      => 'post-php',
				'props'          => array(
					'product_count' => $this->get_product_count(),
				),
			)
		);
	}

	/**
	 * Enqueue the CES survey trigger for an existing product.
	 */
	private function enqueue_ces_survey_for_edited_product() {
		if ( $this->has_been_shown( self::PRODUCT_UPDATE_ACTION_NAME ) ) {
			return;
		}

		$this->enqueue_to_ces_tracks(
			array(
				'action'         => self::PRODUCT_UPDATE_ACTION_NAME,
				'label'          => __(
					'How easy was it to edit your product?',
					'woocommerce'
				),
				'onsubmit_label' => $this->onsubmit_label,
				'pagenow'        => 'product',
				'adminpage'      => 'post-php',
				'props'          => array(
					'product_count' => $this->get_product_count(),
				),
			)
		);
	}

	/**
	 * Enqueue the CES survey trigger for an existing shop order.
	 */
	private function enqueue_ces_survey_for_edited_shop_order() {
		if ( $this->has_been_shown( self::SHOP_ORDER_UPDATE_ACTION_NAME ) ) {
			return;
		}

		$this->enqueue_to_ces_tracks(
			array(
				'action'         => self::SHOP_ORDER_UPDATE_ACTION_NAME,
				'label'          => __(
					'How easy was it to update an order?',
					'woocommerce'
				),
				'onsubmit_label' => $this->onsubmit_label,
				'pagenow'        => 'shop_order',
				'adminpage'      => 'post-php',
				'props'          => array(
					'order_count' => $this->get_shop_order_count(),
				),
			)
		);
	}

	/**
	 * Maybe clear the CES tracks queue, executed on every page load. If the
	 * clear option is set it clears the queue. In practice, this executes a
	 * page load after the queued CES tracks are displayed on the client, which
	 * sets the clear option.
	 */
	public function maybe_clear_ces_tracks_queue() {
		$clear_ces_tracks_queue_for_page = get_option(
			self::CLEAR_CES_TRACKS_QUEUE_FOR_PAGE_OPTION_NAME,
			false
		);

		if ( ! $clear_ces_tracks_queue_for_page ) {
			return;
		}

		$queue           = get_option(
			self::CES_TRACKS_QUEUE_OPTION_NAME,
			array()
		);
		$remaining_items = array_filter(
			$queue,
			function ( $item ) use ( $clear_ces_tracks_queue_for_page ) {
				return $clear_ces_tracks_queue_for_page['pagenow'] !== $item['pagenow']
				|| $clear_ces_tracks_queue_for_page['adminpage'] !== $item['adminpage'];
			}
		);

		update_option(
			self::CES_TRACKS_QUEUE_OPTION_NAME,
			array_values( $remaining_items )
		);
		update_option( self::CLEAR_CES_TRACKS_QUEUE_FOR_PAGE_OPTION_NAME, false );
	}

	/**
	 * Appends a script to footer to trigger CES on adding product categories.
	 */
	public function add_script_track_product_categories() {
		if ( $this->has_been_shown( self::ADD_PRODUCT_CATEGORIES_ACTION_NAME ) ) {
			return;
		}

		wc_enqueue_js(
			$this->get_script_track_edit_php(
				self::ADD_PRODUCT_CATEGORIES_ACTION_NAME,
				__( 'How easy was it to add product category?', 'woocommerce' )
			)
		);
	}

	/**
	 * Appends a script to footer to trigger CES on adding product tags.
	 */
	public function add_script_track_product_tags() {
		if ( $this->has_been_shown( self::ADD_PRODUCT_TAGS_ACTION_NAME ) ) {
			return;
		}

		wc_enqueue_js(
			$this->get_script_track_edit_php(
				self::ADD_PRODUCT_TAGS_ACTION_NAME,
				__( 'How easy was it to add a product tag?', 'woocommerce' )
			)
		);
	}

	/**
	 * Maybe enqueue the CES survey on product import, if step is done.
	 */
	public function run_on_product_import() {
		// We're only interested in when the importer completes.
		if ( empty( $_GET['step'] ) || 'done' !== $_GET['step'] ) { // phpcs:ignore CSRF ok.
			return;
		}

		if ( $this->has_been_shown( self::IMPORT_PRODUCTS_ACTION_NAME ) ) {
			return;
		}

		$this->enqueue_to_ces_tracks(
			array(
				'action'         => self::IMPORT_PRODUCTS_ACTION_NAME,
				'label'          => __(
					'How easy was it to import products?',
					'woocommerce'
				),
				'onsubmit_label' => $this->onsubmit_label,
				'pagenow'        => 'product_page_product_importer',
				'adminpage'      => 'product_page_product_importer',
				'props'          => (object) array(),
			)
		);
	}

	/**
	 * Enqueue the CES survey trigger for setting changes.
	 */
	public function run_on_update_options() {
		// $current_tab is set when WC_Admin_Settings::save_settings is called.
		global $current_tab;
		global $current_section;

		if ( $this->has_been_shown( self::SETTINGS_CHANGE_ACTION_NAME ) ) {
			return;
		}

		$props = array(
			'settings_area' => $current_tab,
		);

		if ( $current_section ) {
			$props['settings_section'] = $current_section;
		}

		$this->enqueue_to_ces_tracks(
			array(
				'action'         => self::SETTINGS_CHANGE_ACTION_NAME,
				'label'          => __(
					'How easy was it to update your settings?',
					'woocommerce'
				),
				'onsubmit_label' => $this->onsubmit_label,
				'pagenow'        => 'woocommerce_page_wc-settings',
				'adminpage'      => 'woocommerce_page_wc-settings',
				'props'          => (object) $props,
			)
		);
	}

	/**
	 * Enqueue the CES survey on adding new product attributes.
	 */
	public function run_on_add_product_attributes() {
		if ( $this->has_been_shown( self::ADD_PRODUCT_ATTRIBUTES_ACTION_NAME ) ) {
			return;
		}

		$this->enqueue_to_ces_tracks(
			array(
				'action'         => self::ADD_PRODUCT_ATTRIBUTES_ACTION_NAME,
				'label'          => __(
					'How easy was it to add a product attribute?',
					'woocommerce'
				),
				'onsubmit_label' => $this->onsubmit_label,
				'pagenow'        => 'product_page_product_attributes',
				'adminpage'      => 'product_page_product_attributes',
				'props'          => (object) array(),
			)
		);
	}

	/**
	 * Determine on initiating CES survey on searching for product or orders.
	 */
	public function run_on_load_edit_php() {
		$allowed_types = array( 'product', 'shop_order' );
		$post_type     = get_current_screen()->post_type;

		// We're only interested for certain post types.
		if ( ! in_array( $post_type, $allowed_types, true ) ) {
			return;
		}

		// Determine whether request is search by "s" GET parameter.
		if ( empty( $_GET['s'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended
			return;
		}

		$page_now = 'edit-' . $post_type;
		$this->enqueue_ces_survey_for_search( $post_type, $page_now, 'edit-php' );
	}
}
Internal/Admin/ShippingLabelBannerDisplayRules.php000064400000012266152333011040016270 0ustar00<?php
/**
 * WooCommerce Shipping Label Banner Display Rules.
 */

namespace Automattic\WooCommerce\Internal\Admin;

/**
 * Determines whether or not the Shipping Label Banner should be displayed
 */
class ShippingLabelBannerDisplayRules {

	/**
	 * Holds the installed Jetpack version.
	 *
	 * @var string
	 */
	private $jetpack_version;


	/**
	 * Whether or not the installed Jetpack is connected.
	 *
	 * @var bool
	 */
	private $jetpack_connected;

	/**
	 * Holds the installed WooCommerce Shipping & Tax version.
	 *
	 * @var string
	 */
	private $wcs_version;

	/**
	 * Whether or not there're plugins installed incompatible with the banner.
	 *
	 * @var bool
	 */
	private $no_incompatible_plugins_installed;

	/**
	 * Whether or not the WooCommerce Shipping & Tax ToS has been accepted.
	 *
	 * @var bool
	 */
	private $wcs_tos_accepted;

	/**
	 * Minimum supported Jetpack version.
	 *
	 * @var string
	 */
	private $min_jetpack_version = '4.4';

	/**
	 * Minimum supported WooCommerce Shipping & Tax version.
	 *
	 * @var string
	 */
	private $min_wcs_version = '1.22.5';

	/**
	 * Supported countries by USPS, see: https://webpmt.usps.gov/pmt010.cfm
	 *
	 * @var array
	 */
	private $supported_countries = array( 'US', 'AS', 'PR', 'VI', 'GU', 'MP', 'UM', 'FM', 'MH' );

	/**
	 * Array of supported currency codes.
	 *
	 * @var array
	 */
	private $supported_currencies = array( 'USD' );


	/**
	 * Constructor.
	 *
	 * @param string $jetpack_version Installed Jetpack version to check.
	 * @param bool   $jetpack_connected Is Jetpack connected?.
	 * @param string $wcs_version Installed WooCommerce Shipping & Tax version to check.
	 * @param bool   $wcs_tos_accepted WooCommerce Shipping & Tax Terms of Service accepted?.
	 * @param bool   $incompatible_plugins_installed Are there any incompatible plugins installed?.
	 */
	public function __construct( $jetpack_version, $jetpack_connected, $wcs_version, $wcs_tos_accepted, $incompatible_plugins_installed ) {
		$this->jetpack_version                   = $jetpack_version;
		$this->jetpack_connected                 = $jetpack_connected;
		$this->wcs_version                       = $wcs_version;
		$this->wcs_tos_accepted                  = $wcs_tos_accepted;
		$this->no_incompatible_plugins_installed = ! $incompatible_plugins_installed;
	}

	/**
	 * Determines whether banner is eligible for display (does not include a/b logic).
	 */
	public function should_display_banner() {
		return $this->banner_not_dismissed() &&
			$this->jetpack_installed_and_active() &&
			$this->jetpack_up_to_date() &&
			$this->jetpack_connected &&
			$this->no_incompatible_plugins_installed &&
			$this->order_has_shippable_products() &&
			$this->store_in_us_and_usd() &&
			( $this->wcs_not_installed() || (
				$this->wcs_up_to_date() && ! $this->wcs_tos_accepted
			) );
	}

	/**
	 * Checks if the banner was not dismissed by the user.
	 *
	 * @return bool
	 */
	private function banner_not_dismissed() {
		$dismissed_timestamp_ms = get_option( 'woocommerce_shipping_dismissed_timestamp' );

		if ( ! is_numeric( $dismissed_timestamp_ms ) ) {
			return true;
		}
		$dismissed_timestamp_ms = intval( $dismissed_timestamp_ms );
		$dismissed_timestamp    = intval( round( $dismissed_timestamp_ms / 1000 ) );
		$expired_timestamp      = $dismissed_timestamp + 24 * 60 * 60; // 24 hours from click time

		$dismissed_for_good = -1 === $dismissed_timestamp_ms;
		$dismissed_24h      = time() < $expired_timestamp;

		return ! $dismissed_for_good && ! $dismissed_24h;
	}

	/**
	 * Checks if jetpack is installed and active.
	 *
	 * @return bool
	 */
	private function jetpack_installed_and_active() {
		return ! ! $this->jetpack_version;
	}

	/**
	 * Checks if Jetpack version is supported.
	 *
	 * @return bool
	 */
	private function jetpack_up_to_date() {
		return version_compare( $this->jetpack_version, $this->min_jetpack_version, '>=' );
	}

	/**
	 * Checks if there's a shippable product in the current order.
	 *
	 * @return bool
	 */
	private function order_has_shippable_products() {
		$post = get_post();
		if ( ! $post ) {
			return false;
		}

		$order = wc_get_order( get_post()->ID );

		if ( ! $order ) {
			return false;
		}
		// At this point (no packaging data), only show if there's at least one existing and shippable product.
		foreach ( $order->get_items() as $item ) {
			if ( $item instanceof \WC_Order_Item_Product ) {
				$product = $item->get_product();

				if ( $product && $product->needs_shipping() ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Checks if the store is in the US and has its default currency set to USD.
	 *
	 * @return bool
	 */
	private function store_in_us_and_usd() {
		$base_currency = get_woocommerce_currency();
		$base_location = wc_get_base_location();

		return in_array( $base_currency, $this->supported_currencies, true ) && in_array( $base_location['country'], $this->supported_countries, true );
	}

	/**
	 * Checks if WooCommerce Shipping & Tax is not installed.
	 *
	 * @return bool
	 */
	private function wcs_not_installed() {
		return ! $this->wcs_version;
	}

	/**
	 * Checks if WooCommerce Shipping & Tax is up to date.
	 */
	private function wcs_up_to_date() {
		return $this->wcs_version && version_compare( $this->wcs_version, $this->min_wcs_version, '>=' );
	}
}
Internal/Admin/Onboarding/OnboardingMailchimp.php000064400000002301152333011040016035 0ustar00<?php
/**
 * WooCommerce Onboarding Mailchimp
 */

namespace Automattic\WooCommerce\Internal\Admin\Onboarding;

use Automattic\WooCommerce\Internal\Admin\Schedulers\MailchimpScheduler;

/**
 * Logic around updating Mailchimp during onboarding.
 */
class OnboardingMailchimp {
	/**
	 * Class instance.
	 *
	 * @var OnboardingMailchimp instance
	 */
	private static $instance = null;

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Init.
	 */
	public function init() {
		add_action( 'woocommerce_onboarding_profile_data_updated', array( $this, 'on_profile_data_updated' ), 10, 2 );
	}

	/**
	 * Reset MailchimpScheduler if profile data is being updated with a new email.
	 *
	 * @param array $existing_data Existing option data.
	 * @param array $updating_data Updating option data.
	 */
	public function on_profile_data_updated( $existing_data, $updating_data ) {
		if (
			isset( $existing_data['store_email'] ) &&
			isset( $updating_data['store_email'] ) &&
			$existing_data['store_email'] !== $updating_data['store_email']
		) {
			MailchimpScheduler::reset();
		}
	}
}
Internal/Admin/Onboarding/OnboardingProducts.php000064400000013145152333011040015745 0ustar00<?php
/**
 * WooCommerce Onboarding Products
 */

namespace Automattic\WooCommerce\Internal\Admin\Onboarding;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Loader;
use Automattic\WooCommerce\Admin\PluginsHelper;

/**
 * Class for handling product types and data around product types.
 */
class OnboardingProducts {

	/**
	 * Name of product data transient.
	 *
	 * @var string
	 */
	const PRODUCT_DATA_TRANSIENT = 'wc_onboarding_product_data';

	/**
	 * Get a list of allowed product types for the onboarding wizard.
	 *
	 * @return array
	 */
	public static function get_allowed_product_types() {
		$products         = array(
			'physical'        => array(
				'label'   => __( 'Physical products', 'woocommerce' ),
				'default' => true,
			),
			'downloads'       => array(
				'label' => __( 'Downloads', 'woocommerce' ),
			),
			'subscriptions'   => array(
				'label' => __( 'Subscriptions', 'woocommerce' ),
			),
			'memberships'     => array(
				'label'   => __( 'Memberships', 'woocommerce' ),
				'product' => 958589,
			),
			'bookings'        => array(
				'label'   => __( 'Bookings', 'woocommerce' ),
				'product' => 390890,
			),
			'product-bundles' => array(
				'label'   => __( 'Bundles', 'woocommerce' ),
				'product' => 18716,
			),
			'product-add-ons' => array(
				'label'   => __( 'Customizable products', 'woocommerce' ),
				'product' => 18618,
			),
		);
		$base_location    = wc_get_base_location();
		$has_cbd_industry = false;
		if ( 'US' === $base_location['country'] ) {
			$profile = get_option( OnboardingProfile::DATA_OPTION, array() );
			if ( ! empty( $profile['industry'] ) ) {
				$has_cbd_industry = in_array( 'cbd-other-hemp-derived-products', array_column( $profile['industry'], 'slug' ), true );
			}
		}
		if ( ! Features::is_enabled( 'subscriptions' ) || 'US' !== $base_location['country'] || $has_cbd_industry ) {
			$products['subscriptions']['product'] = 27147;
		}

		return apply_filters( 'woocommerce_admin_onboarding_product_types', $products );
	}

	/**
	 * Get dynamic product data from API.
	 *
	 * @param array $product_types Array of product types.
	 * @return array
	 */
	public static function get_product_data( $product_types ) {
		$locale = get_user_locale();
		// Transient value is an array of product data keyed by locale.
		$transient_value      = get_transient( self::PRODUCT_DATA_TRANSIENT );
		$transient_value      = is_array( $transient_value ) ? $transient_value : array();
		$woocommerce_products = $transient_value[ $locale ] ?? false;

		if ( false === $woocommerce_products ) {
			$woocommerce_products = wp_remote_get(
				add_query_arg(
					array(
						'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
						'locale'     => $locale,
					),
					'https://woocommerce.com/wp-json/wccom-extensions/1.0/search'
				)
			);
			if ( is_wp_error( $woocommerce_products ) ) {
				return $product_types;
			}
			$transient_value[ $locale ] = $woocommerce_products;
			set_transient( self::PRODUCT_DATA_TRANSIENT, $transient_value, DAY_IN_SECONDS );
		}

		$data         = json_decode( $woocommerce_products['body'] );
		$products     = array();
		$product_data = array();

		// Map product data by ID.
		if ( isset( $data ) && isset( $data->products ) ) {
			foreach ( $data->products as $product_datum ) {
				if ( isset( $product_datum->id ) ) {
					$products[ $product_datum->id ] = $product_datum;
				}
			}
		}

		// Loop over product types and append data.
		foreach ( $product_types as $key => $product_type ) {
			$product_data[ $key ] = $product_types[ $key ];

			if ( isset( $product_type['product'] ) && isset( $products[ $product_type['product'] ] ) ) {
				$price        = html_entity_decode( $products[ $product_type['product'] ]->price );
				$yearly_price = (float) str_replace( '$', '', $price );

				$product_data[ $key ]['yearly_price'] = $yearly_price;
				$product_data[ $key ]['description']  = $products[ $product_type['product'] ]->excerpt;
				$product_data[ $key ]['more_url']     = $products[ $product_type['product'] ]->link;
				$product_data[ $key ]['slug']         = strtolower( preg_replace( '~[^\pL\d]+~u', '-', $products[ $product_type['product'] ]->slug ) );
			} elseif ( isset( $product_type['product'] ) ) {
				/* translators: site currency symbol (used to show that the product costs money) */
				$product_data[ $key ]['label'] .= sprintf( __( ' — %s', 'woocommerce' ), html_entity_decode( get_woocommerce_currency_symbol() ) );
			}
		}

		return $product_data;
	}

	/**
	 * Get the allowed product types with the polled data.
	 *
	 * @return array
	 */
	public static function get_product_types_with_data() {
		return self::get_product_data( self::get_allowed_product_types() );
	}

	/**
	 * Get relevant purchaseable products for the site.
	 *
	 * @return array
	 */
	public static function get_relevant_products() {
		$profiler_data = get_option( OnboardingProfile::DATA_OPTION, array() );
		$installed     = PluginsHelper::get_installed_plugin_slugs();
		$product_types = isset( $profiler_data['product_types'] ) ? $profiler_data['product_types'] : array();
		$product_data  = self::get_product_types_with_data();
		$purchaseable  = array();
		$remaining     = array();
		foreach ( $product_types as $type ) {
			if ( ! isset( $product_data[ $type ]['slug'] ) ) {
				continue;
			}

			$purchaseable[] = $product_data[ $type ];

			if ( ! in_array( $product_data[ $type ]['slug'], $installed, true ) ) {
				$remaining[] = $product_data[ $type ]['label'];
			}
		}

		return array(
			'purchaseable' => $purchaseable,
			'remaining'    => $remaining,
		);
	}
}
Internal/Admin/Onboarding/OnboardingProfile.php000064400000006152152333011040015542 0ustar00<?php
/**
 * WooCommerce Onboarding Setup Wizard
 */

namespace Automattic\WooCommerce\Internal\Admin\Onboarding;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Admin\WCAdminHelper;

/**
 * Contains backend logic for the onboarding profile and checklist feature.
 */
class OnboardingProfile {
	/**
	 * Profile data option name.
	 */
	const DATA_OPTION = 'woocommerce_onboarding_profile';

	/**
	 * Add onboarding actions.
	 */
	public static function init() {
		add_action( 'woocommerce_updated', array( __CLASS__, 'maybe_mark_complete' ) );
		add_action( 'update_option_' . self::DATA_OPTION, array( __CLASS__, 'trigger_complete' ), 10, 2 );
	}

	/**
	 * Trigger the woocommerce_onboarding_profile_completed action
	 *
	 * @param array $old_value Previous value.
	 * @param array $value Current value.
	 */
	public static function trigger_complete( $old_value, $value ) {
		if ( isset( $old_value['completed'] ) && $old_value['completed'] ) {
			return;
		}

		if ( ! isset( $value['completed'] ) || ! $value['completed'] ) {
			return;
		}

		/**
		 * Action hook fired when the onboarding profile (or onboarding wizard,
		 * or profiler) is completed.
		 *
		 * @since 1.5.0
		 */
		do_action( 'woocommerce_onboarding_profile_completed' );
	}

	/**
	 * Check if the profiler still needs to be completed.
	 *
	 * @return bool
	 */
	public static function needs_completion() {
		$onboarding_data = get_option( self::DATA_OPTION, array() );

		$is_completed = isset( $onboarding_data['completed'] ) && true === $onboarding_data['completed'];
		$is_skipped   = isset( $onboarding_data['skipped'] ) && true === $onboarding_data['skipped'];

		// @todo When merging to WooCommerce Core, we should set the `completed` flag to true during the upgrade progress.
		// https://github.com/woocommerce/woocommerce-admin/pull/2300#discussion_r287237498.
		return ! $is_completed && ! $is_skipped;
	}

	/**
	 * When updating WooCommerce, mark the profiler and task list complete.
	 *
	 * @todo The `maybe_enable_setup_wizard()` method should be revamped on onboarding enable in core.
	 * See https://github.com/woocommerce/woocommerce/blob/1ca791f8f2325fe2ee0947b9c47e6a4627366374/includes/class-wc-install.php#L341
	 */
	public static function maybe_mark_complete() {
		// The install notice still exists so don't complete the profiler.
		if ( ! class_exists( 'WC_Admin_Notices' ) || \WC_Admin_Notices::has_notice( 'install' ) ) {
			return;
		}

		$onboarding_data = get_option( self::DATA_OPTION, array() );
		// Don't make updates if the profiler is completed or skipped, but task list is potentially incomplete.
		if (
			( isset( $onboarding_data['completed'] ) && $onboarding_data['completed'] ) ||
			( isset( $onboarding_data['skipped'] ) && $onboarding_data['skipped'] )
		) {
			return;
		}

		$onboarding_data['completed'] = true;
		update_option( self::DATA_OPTION, $onboarding_data );

		if ( ! WCAdminHelper::is_wc_admin_active_for( DAY_IN_SECONDS ) ) {
			$task_list = TaskLists::get_list( 'setup' );
			if ( ! $task_list ) {
				return;
			}
			$task_list->hide();
		}
	}
}
Internal/Admin/Onboarding/OnboardingHelper.php000064400000011502152333011040015354 0ustar00<?php
/**
 * WooCommerce Onboarding Helper
 */

namespace Automattic\WooCommerce\Internal\Admin\Onboarding;

use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;

/**
 * Contains backend logic for the onboarding profile and checklist feature.
 */
class OnboardingHelper {

	/**
	 * Class instance.
	 *
	 * @var OnboardingHelper instance
	 */
	private static $instance = null;

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Init.
	 */
	public function init() {
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'current_screen', array( $this, 'add_help_tab' ), 60 );
		add_action( 'current_screen', array( $this, 'reset_task_list' ) );
		add_action( 'current_screen', array( $this, 'reset_extended_task_list' ) );
	}

	/**
	 * Update the help tab setup link to reset the onboarding profiler.
	 */
	public function add_help_tab() {
		if ( ! function_exists( 'wc_get_screen_ids' ) ) {
			return;
		}

		$screen = get_current_screen();

		if ( ! $screen || ! in_array( $screen->id, wc_get_screen_ids(), true ) ) {
			return;
		}

		// Remove the old help tab if it exists.
		$help_tabs = $screen->get_help_tabs();
		foreach ( $help_tabs as $help_tab ) {
			if ( 'woocommerce_onboard_tab' !== $help_tab['id'] ) {
				continue;
			}

			$screen->remove_help_tab( 'woocommerce_onboard_tab' );
		}

		// Add the new help tab.
		$help_tab = array(
			'title' => __( 'Setup wizard', 'woocommerce' ),
			'id'    => 'woocommerce_onboard_tab',
		);

		$setup_list    = TaskLists::get_list( 'setup' );
		$extended_list = TaskLists::get_list( 'extended' );

		if ( $setup_list ) {
			$help_tab['content'] = '<h2>' . __( 'WooCommerce Onboarding', 'woocommerce' ) . '</h2>';

			$help_tab['content'] .= '<h3>' . __( 'Profile Setup Wizard', 'woocommerce' ) . '</h3>';
			$help_tab['content'] .= '<p>' . __( 'If you need to access the setup wizard again, please click on the button below.', 'woocommerce' ) . '</p>' .
				'<p><a href="' . wc_admin_url( '&path=/setup-wizard' ) . '" class="button button-primary">' . __( 'Setup wizard', 'woocommerce' ) . '</a></p>';

			$help_tab['content'] .= '<h3>' . __( 'Task List', 'woocommerce' ) . '</h3>';
			$help_tab['content'] .= '<p>' . __( 'If you need to enable or disable the task lists, please click on the button below.', 'woocommerce' ) . '</p>' .
			( $setup_list->is_hidden()
				? '<p><a href="' . wc_admin_url( '&reset_task_list=1' ) . '" class="button button-primary">' . __( 'Enable', 'woocommerce' ) . '</a></p>'
				: '<p><a href="' . wc_admin_url( '&reset_task_list=0' ) . '" class="button button-primary">' . __( 'Disable', 'woocommerce' ) . '</a></p>'
			);
		}

		if ( $extended_list ) {
			$help_tab['content'] .= '<h3>' . __( 'Extended task List', 'woocommerce' ) . '</h3>';
			$help_tab['content'] .= '<p>' . __( 'If you need to enable or disable the extended task lists, please click on the button below.', 'woocommerce' ) . '</p>' .
			( $extended_list->is_hidden()
				? '<p><a href="' . wc_admin_url( '&reset_extended_task_list=1' ) . '" class="button button-primary">' . __( 'Enable', 'woocommerce' ) . '</a></p>'
				: '<p><a href="' . wc_admin_url( '&reset_extended_task_list=0' ) . '" class="button button-primary">' . __( 'Disable', 'woocommerce' ) . '</a></p>'
			);
		}

		$screen->add_help_tab( $help_tab );
	}

	/**
	 * Reset the onboarding task list and redirect to the dashboard.
	 */
	public function reset_task_list() {
		if (
			! PageController::is_admin_page() ||
			! isset( $_GET['reset_task_list'] ) // phpcs:ignore CSRF ok.
		) {
			return;
		}

		$task_list = TaskLists::get_list( 'setup' );

		if ( ! $task_list ) {
			return;
		}
		$show   = 1 === absint( $_GET['reset_task_list'] ); // phpcs:ignore CSRF ok.
		$update = $show ? $task_list->unhide() : $task_list->hide(); // phpcs:ignore CSRF ok.

		if ( $update ) {
			wc_admin_record_tracks_event(
				'tasklist_toggled',
				array(
					'status' => $show ? 'enabled' : 'disabled',
				)
			);
		}

		wp_safe_redirect( wc_admin_url() );
		exit;
	}

	/**
	 * Reset the extended task list and redirect to the dashboard.
	 */
	public function reset_extended_task_list() {
		if (
			! PageController::is_admin_page() ||
			! isset( $_GET['reset_extended_task_list'] ) // phpcs:ignore CSRF ok.
		) {
			return;
		}

		$task_list = TaskLists::get_list( 'extended' );

		if ( ! $task_list ) {
			return;
		}
		$show   = 1 === absint( $_GET['reset_extended_task_list'] ); // phpcs:ignore CSRF ok.
		$update = $show ? $task_list->unhide() : $task_list->hide(); // phpcs:ignore CSRF ok.

		if ( $update ) {
			wc_admin_record_tracks_event(
				'extended_tasklist_toggled',
				array(
					'status' => $show ? 'disabled' : 'enabled',
				)
			);
		}

		wp_safe_redirect( wc_admin_url() );
		exit;
	}
}
Internal/Admin/Onboarding/Onboarding.php000064400000001073152333011040014216 0ustar00<?php
/**
 * WooCommerce Onboarding
 */

namespace Automattic\WooCommerce\Internal\Admin\Onboarding;

/**
 * Initializes backend logic for the onboarding process.
 */
class Onboarding {
	/**
	 * Initialize onboarding functionality.
	 */
	public static function init() {
		OnboardingHelper::instance()->init();
		OnboardingIndustries::init();
		OnboardingJetpack::instance()->init();
		OnboardingMailchimp::instance()->init();
		OnboardingProfile::init();
		OnboardingSetupWizard::instance()->init();
		OnboardingSync::instance()->init();
		OnboardingThemes::init();
	}
}
Internal/Admin/Onboarding/OnboardingSetupWizard.php000064400000020246152333011040016423 0ustar00<?php
/**
 * WooCommerce Onboarding Setup Wizard
 */

namespace Automattic\WooCommerce\Internal\Admin\Onboarding;

use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Admin\WCAdminHelper;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;

/**
 * Contains backend logic for the onboarding profile and checklist feature.
 */
class OnboardingSetupWizard {
	/**
	 * Class instance.
	 *
	 * @var OnboardingSetupWizard instance
	 */
	private static $instance = null;

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Add onboarding actions.
	 */
	public function init() {
		if ( ! is_admin() ) {
			return;
		}

		// Old settings injection.
		// Run after Automattic\WooCommerce\Internal\Admin\Loader.
		add_filter( 'woocommerce_components_settings', array( $this, 'component_settings' ), 20 );
		// New settings injection.
		add_filter( 'woocommerce_admin_shared_settings', array( $this, 'component_settings' ), 20 );
		add_filter( 'woocommerce_admin_preload_settings', array( $this, 'preload_settings' ) );
		add_filter( 'admin_body_class', array( $this, 'add_loading_classes' ) );
		add_action( 'admin_init', array( $this, 'do_admin_redirects' ) );
		add_action( 'current_screen', array( $this, 'redirect_to_profiler' ) );
		add_filter( 'woocommerce_show_admin_notice', array( $this, 'remove_old_install_notice' ), 10, 2 );
	}

	/**
	 * Test whether the context of execution comes from async action scheduler.
	 * Note: this is a polyfill for wc_is_running_from_async_action_scheduler()
	 *       which was introduced in WC 4.0.
	 *
	 * @return bool
	 */
	private function is_running_from_async_action_scheduler() {
		if ( function_exists( '\wc_is_running_from_async_action_scheduler' ) ) {
			return \wc_is_running_from_async_action_scheduler();
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		return isset( $_REQUEST['action'] ) && 'as_async_request_queue_runner' === $_REQUEST['action'];
	}

	/**
	 * Handle redirects to setup/welcome page after install and updates.
	 *
	 * For setup wizard, transient must be present, the user must have access rights, and we must ignore the network/bulk plugin updaters.
	 */
	public function do_admin_redirects() {
		// Don't run this fn from Action Scheduler requests, as it would clear _wc_activation_redirect transient.
		// That means OBW would never be shown.
		if ( $this->is_running_from_async_action_scheduler() ) {
			return;
		}

		// Setup wizard redirect.
		if ( get_transient( '_wc_activation_redirect' ) && apply_filters( 'woocommerce_enable_setup_wizard', true ) ) {
			$do_redirect        = true;
			$current_page       = isset( $_GET['page'] ) ? wc_clean( wp_unslash( $_GET['page'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification
			$is_onboarding_path = ! isset( $_GET['path'] ) || '/setup-wizard' === wc_clean( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification

			// On these pages, or during these events, postpone the redirect.
			if ( wp_doing_ajax() || is_network_admin() || ! current_user_can( 'manage_woocommerce' ) ) {
				$do_redirect = false;
			}

			// On these pages, or during these events, disable the redirect.
			if (
				( 'wc-admin' === $current_page && $is_onboarding_path ) ||
				apply_filters( 'woocommerce_prevent_automatic_wizard_redirect', false ) ||
				isset( $_GET['activate-multi'] ) // phpcs:ignore WordPress.Security.NonceVerification
			) {
				delete_transient( '_wc_activation_redirect' );
				$do_redirect = false;
			}

			if ( $do_redirect ) {
				delete_transient( '_wc_activation_redirect' );
				wp_safe_redirect( wc_admin_url() );
				exit;
			}
		}
	}

	/**
	 * Trigger the woocommerce_onboarding_profile_completed action
	 *
	 * @param array $old_value Previous value.
	 * @param array $value Current value.
	 */
	public function trigger_profile_completed_action( $old_value, $value ) {
		if ( isset( $old_value['completed'] ) && $old_value['completed'] ) {
			return;
		}

		if ( ! isset( $value['completed'] ) || ! $value['completed'] ) {
			return;
		}

		/**
		 * Action hook fired when the onboarding profile (or onboarding wizard,
		 * or profiler) is completed.
		 *
		 * @since 1.5.0
		 */
		do_action( 'woocommerce_onboarding_profile_completed' );
	}

	/**
	 * Returns true if the profiler should be displayed (not completed and not skipped).
	 *
	 * @return bool
	 */
	private function should_show() {
		if ( $this->is_setup_wizard() ) {
			return true;
		}

		return OnboardingProfile::needs_completion();
	}

	/**
	 * Redirect to the profiler on homepage if completion is needed.
	 */
	public function redirect_to_profiler() {
		if ( ! $this->is_homepage() || ! OnboardingProfile::needs_completion() ) {
			return;
		}

		wp_safe_redirect( wc_admin_url( '&path=/setup-wizard' ) );
		exit;
	}

	/**
	 * Check if the current page is the profile wizard.
	 *
	 * @return bool
	 */
	private function is_setup_wizard() {
		/* phpcs:disable WordPress.Security.NonceVerification */
		return isset( $_GET['page'] ) &&
			'wc-admin' === $_GET['page'] &&
			isset( $_GET['path'] ) &&
			'/setup-wizard' === $_GET['path'];
		/* phpcs: enable */
	}

	/**
	 * Check if the current page is the homepage.
	 *
	 * @return bool
	 */
	private function is_homepage() {
		/* phpcs:disable WordPress.Security.NonceVerification */
		return isset( $_GET['page'] ) &&
			'wc-admin' === $_GET['page'] &&
			! isset( $_GET['path'] );
		/* phpcs: enable */
	}

	/**
	 * Determine if the current page is one of the WC Admin pages.
	 *
	 * @return bool
	 */
	private function is_woocommerce_page() {
		$current_page = PageController::get_instance()->get_current_page();
		if ( ! $current_page || ! isset( $current_page['path'] ) ) {
			return false;
		}

		return 0 === strpos( $current_page['path'], 'wc-admin' );
	}

	/**
	 * Add profiler items to component settings.
	 *
	 * @param array $settings Component settings.
	 *
	 * @return array
	 */
	public function component_settings( $settings ) {
		$profile                = (array) get_option( OnboardingProfile::DATA_OPTION, array() );
		$settings['onboarding'] = array(
			'profile' => $profile,
		);

		// Only fetch if the onboarding wizard OR the task list is incomplete or currently shown
		// or the current page is one of the WooCommerce Admin pages.
		if (
			( ! $this->should_show() && ! count( TaskLists::get_visible() )
			||
			! $this->is_woocommerce_page()
		)
		) {
			return $settings;
		}

		include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-options.php';
		$wccom_auth                 = \WC_Helper_Options::get( 'auth' );
		$profile['wccom_connected'] = empty( $wccom_auth['access_token'] ) ? false : true;

		$settings['onboarding']['currencySymbols'] = get_woocommerce_currency_symbols();
		$settings['onboarding']['euCountries']     = WC()->countries->get_european_union_countries();
		$settings['onboarding']['localeInfo']      = include WC()->plugin_path() . '/i18n/locale-info.php';
		$settings['onboarding']['profile']         = $profile;

		return apply_filters( 'woocommerce_admin_onboarding_preloaded_data', $settings );
	}

	/**
	 * Preload WC setting options to prime state of the application.
	 *
	 * @param array $options Array of options to preload.
	 * @return array
	 */
	public function preload_settings( $options ) {
		$options[] = 'general';

		return $options;
	}

	/**
	 * Set the admin full screen class when loading to prevent flashes of unstyled content.
	 *
	 * @param bool $classes Body classes.
	 * @return array
	 */
	public function add_loading_classes( $classes ) {
		/* phpcs:disable WordPress.Security.NonceVerification */
		if ( $this->is_setup_wizard() ) {
			$classes .= ' woocommerce-admin-full-screen';
		}
		/* phpcs: enable */

		return $classes;
	}

	/**
	 * Remove the install notice that prompts the user to visit the old onboarding setup wizard.
	 *
	 * @param bool   $show Show or hide the notice.
	 * @param string $notice The slug of the notice.
	 * @return bool
	 */
	public function remove_old_install_notice( $show, $notice ) {
		if ( 'install' === $notice ) {
			return false;
		}

		return $show;
	}
}
Internal/Admin/Onboarding/OnboardingSync.php000064400000007721152333011040015061 0ustar00<?php
/**
 * WooCommerce Onboarding
 */

namespace Automattic\WooCommerce\Internal\Admin\Onboarding;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;

/**
 * Contains backend logic for the onboarding profile and checklist feature.
 */
class OnboardingSync {
	/**
	 * Class instance.
	 *
	 * @var OnboardingSync instance
	 */
	private static $instance = null;

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Init.
	 */
	public function init() {
		add_action( 'update_option_' . OnboardingProfile::DATA_OPTION, array( $this, 'send_profile_data_on_update' ), 10, 2 );
		add_action( 'woocommerce_helper_connected', array( $this, 'send_profile_data_on_connect' ) );

		if ( ! is_admin() ) {
			return;
		}

		add_action( 'current_screen', array( $this, 'redirect_wccom_install' ) );
	}

	/**
	 * Send profile data to WooCommerce.com.
	 */
	private function send_profile_data() {
		if ( 'yes' !== get_option( 'woocommerce_allow_tracking', 'no' ) ) {
			return;
		}

		if ( ! class_exists( '\WC_Helper_API' ) || ! method_exists( '\WC_Helper_API', 'put' ) ) {
			return;
		}

		if ( ! class_exists( '\WC_Helper_Options' ) ) {
			return;
		}

		$auth = \WC_Helper_Options::get( 'auth' );
		if ( empty( $auth['access_token'] ) || empty( $auth['access_token_secret'] ) ) {
			return false;
		}

		$profile       = get_option( OnboardingProfile::DATA_OPTION, array() );
		$base_location = wc_get_base_location();
		$defaults      = array(
			'plugins'             => 'skipped',
			'industry'            => array(),
			'product_types'       => array(),
			'product_count'       => '0',
			'selling_venues'      => 'no',
			'number_employees'    => '1',
			'revenue'             => 'none',
			'other_platform'      => 'none',
			'business_extensions' => array(),
			'theme'               => get_stylesheet(),
			'setup_client'        => false,
			'store_location'      => $base_location['country'],
			'default_currency'    => get_woocommerce_currency(),
		);

		// Prepare industries as an array of slugs if they are in array format.
		if ( isset( $profile['industry'] ) && is_array( $profile['industry'] ) ) {
			$industry_slugs = array();
			foreach ( $profile['industry'] as $industry ) {
				$industry_slugs[] = is_array( $industry ) ? $industry['slug'] : $industry;
			}
			$profile['industry'] = $industry_slugs;
		}
		$body = wp_parse_args( $profile, $defaults );

		\WC_Helper_API::put(
			'profile',
			array(
				'authenticated' => true,
				'body'          => wp_json_encode( $body ),
				'headers'       => array(
					'Content-Type' => 'application/json',
				),
			)
		);
	}

	/**
	 * Send profiler data on profiler change to completion.
	 *
	 * @param array $old_value Previous value.
	 * @param array $value Current value.
	 */
	public function send_profile_data_on_update( $old_value, $value ) {
		if ( ! isset( $value['completed'] ) || ! $value['completed'] ) {
			return;
		}

		$this->send_profile_data();
	}

	/**
	 * Send profiler data after a site is connected.
	 */
	public function send_profile_data_on_connect() {
		$profile = get_option( OnboardingProfile::DATA_OPTION, array() );
		if ( ! isset( $profile['completed'] ) || ! $profile['completed'] ) {
			return;
		}

		$this->send_profile_data();
	}

	/**
	 * Redirects the user to the task list if the task list is enabled and finishing a wccom checkout.
	 *
	 * @todo Once URL params are added to the redirect, we can check those instead of the referer.
	 */
	public function redirect_wccom_install() {
		$task_list = TaskLists::get_list( 'setup' );

		if (
			! $task_list ||
			$task_list->is_hidden() ||
			! isset( $_SERVER['HTTP_REFERER'] ) ||
			0 !== strpos( $_SERVER['HTTP_REFERER'], 'https://woocommerce.com/checkout?utm_medium=product' ) // phpcs:ignore sanitization ok.
		) {
			return;
		}

		wp_safe_redirect( wc_admin_url() );
	}
}
Internal/Admin/Onboarding/OnboardingIndustries.php000064400000005020152333011040016264 0ustar00<?php
/**
 * WooCommerce Onboarding Industries
 */

namespace Automattic\WooCommerce\Internal\Admin\Onboarding;

/**
 * Logic around onboarding industries.
 */
class OnboardingIndustries {
	/**
	 * Init.
	 */
	public static function init() {
		add_filter( 'woocommerce_admin_onboarding_preloaded_data', array( __CLASS__, 'preload_data' ) );
	}

	/**
	 * Get a list of allowed industries for the onboarding wizard.
	 *
	 * @return array
	 */
	public static function get_allowed_industries() {
		/* With "use_description" we turn the description input on. With "description_label" we set the input label */
		return apply_filters(
			'woocommerce_admin_onboarding_industries',
			array(
				'fashion-apparel-accessories'     => array(
					'label'             => __( 'Fashion, apparel, and accessories', 'woocommerce' ),
					'use_description'   => false,
					'description_label' => '',
				),
				'health-beauty'                   => array(
					'label'             => __( 'Health and beauty', 'woocommerce' ),
					'use_description'   => false,
					'description_label' => '',
				),
				'electronics-computers'           => array(
					'label'             => __( 'Electronics and computers', 'woocommerce' ),
					'use_description'   => false,
					'description_label' => '',
				),
				'food-drink'                      => array(
					'label'             => __( 'Food and drink', 'woocommerce' ),
					'use_description'   => false,
					'description_label' => '',
				),
				'home-furniture-garden'           => array(
					'label'             => __( 'Home, furniture, and garden', 'woocommerce' ),
					'use_description'   => false,
					'description_label' => '',
				),
				'cbd-other-hemp-derived-products' => array(
					'label'             => __( 'CBD and other hemp-derived products', 'woocommerce' ),
					'use_description'   => false,
					'description_label' => '',
				),
				'education-and-learning'          => array(
					'label'             => __( 'Education and learning', 'woocommerce' ),
					'use_description'   => false,
					'description_label' => '',
				),
				'other'                           => array(
					'label'             => __( 'Other', 'woocommerce' ),
					'use_description'   => true,
					'description_label' => __( 'Description', 'woocommerce' ),
				),
			)
		);
	}

	/**
	 * Add preloaded data to onboarding.
	 *
	 * @param array $settings Component settings.
	 * @return array
	 */
	public static function preload_data( $settings ) {
		$settings['onboarding']['industries'] = self::get_allowed_industries();
		return $settings;
	}
}
Internal/Admin/Onboarding/OnboardingThemes.php000064400000015312152333011040015365 0ustar00<?php
/**
 * WooCommerce Onboarding Themes
 */

namespace Automattic\WooCommerce\Internal\Admin\Onboarding;

use \Automattic\WooCommerce\Admin\Loader;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Admin\WCAdminHelper;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Init as OnboardingTasks;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
use Automattic\WooCommerce\Admin\Schedulers\MailchimpScheduler;

/**
 * Logic around onboarding themes.
 */
class OnboardingThemes {
	/**
	 * Name of themes transient.
	 *
	 * @var string
	 */
	const THEMES_TRANSIENT = 'wc_onboarding_themes';

	/**
	 * Init.
	 */
	public static function init() {
		add_action( 'woocommerce_theme_installed', array( __CLASS__, 'delete_themes_transient' ) );
		add_action( 'after_switch_theme', array( __CLASS__, 'delete_themes_transient' ) );
		add_filter( 'woocommerce_rest_prepare_themes', array( __CLASS__, 'add_uploaded_theme_data' ) );
		add_filter( 'woocommerce_admin_onboarding_preloaded_data', array( __CLASS__, 'preload_data' ) );
	}

	/**
	 * Get puchasable theme by slug.
	 *
	 * @param string $price_string string of price.
	 * @return float|null
	 */
	private static function get_price_from_string( $price_string ) {
		$price_match = null;
		// Parse price from string as it includes the currency symbol.
		preg_match( '/\\d+\.\d{2}\s*/', $price_string, $price_match );
		if ( count( $price_match ) > 0 ) {
			return (float) $price_match[0];
		}
		return null;
	}

	/**
	 * Get puchasable theme by slug.
	 *
	 * @param string $slug from theme.
	 * @return array|null
	 */
	public static function get_paid_theme_by_slug( $slug ) {
		$themes    = self::get_themes();
		$theme_key = array_search( $slug, array_column( $themes, 'slug' ), true );
		$theme     = false !== $theme_key ? $themes[ $theme_key ] : null;
		if ( $theme && isset( $theme['id'] ) && isset( $theme['price'] ) ) {
			$price = self::get_price_from_string( $theme['price'] );
			if ( $price && $price > 0 ) {
				return $themes[ $theme_key ];
			}
		}
		return null;
	}

	/**
	 * Sort themes returned from WooCommerce.com
	 *
	 * @param  array $themes Array of themes from WooCommerce.com.
	 * @return array
	 */
	public static function sort_woocommerce_themes( $themes ) {
		usort(
			$themes,
			function ( $product_1, $product_2 ) {
				if ( ! property_exists( $product_1, 'id' ) || ! property_exists( $product_1, 'slug' ) ) {
					return 1;
				}
				if ( ! property_exists( $product_2, 'id' ) || ! property_exists( $product_2, 'slug' ) ) {
					return 1;
				}
				if ( in_array( 'Storefront', array( $product_1->slug, $product_2->slug ), true ) ) {
					return 'Storefront' === $product_1->slug ? -1 : 1;
				}
				return $product_1->id < $product_2->id ? 1 : -1;
			}
		);
		return $themes;
	}

	/**
	 * Get a list of themes for the onboarding wizard.
	 *
	 * @return array
	 */
	public static function get_themes() {
		$themes = get_transient( self::THEMES_TRANSIENT );
		if ( false === $themes ) {
			$theme_data = wp_remote_get(
				'https://woocommerce.com/wp-json/wccom-extensions/1.0/search?category=themes',
				array(
					'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
				)
			);
			$themes     = array();

			if ( ! is_wp_error( $theme_data ) ) {
				$theme_data    = json_decode( $theme_data['body'] );
				$woo_themes    = property_exists( $theme_data, 'products' ) ? $theme_data->products : array();
				$sorted_themes = self::sort_woocommerce_themes( $woo_themes );

				foreach ( $sorted_themes as $theme ) {
					$slug                                       = sanitize_title_with_dashes( $theme->slug );
					$themes[ $slug ]                            = (array) $theme;
					$themes[ $slug ]['is_installed']            = false;
					$themes[ $slug ]['has_woocommerce_support'] = true;
					$themes[ $slug ]['slug']                    = $slug;
				}
			}

			$installed_themes = wp_get_themes();
			$active_theme     = get_option( 'stylesheet' );

			foreach ( $installed_themes as $slug => $theme ) {
				$theme_data = self::get_theme_data( $theme );
				if ( isset( $themes[ $slug ] ) ) {
					$themes[ $slug ]['is_installed'] = true;
					$themes[ $slug ]['image']        = $theme_data['image'];
				} else {
					$themes[ $slug ] = $theme_data;
				}
			}

			// Add the WooCommerce support tag for default themes that don't explicitly declare support.
			if ( function_exists( 'wc_is_wp_default_theme_active' ) && wc_is_wp_default_theme_active() ) {
				$themes[ $active_theme ]['has_woocommerce_support'] = true;
			}

			$themes = array( $active_theme => $themes[ $active_theme ] ) + $themes;

			set_transient( self::THEMES_TRANSIENT, $themes, DAY_IN_SECONDS );
		}

		$themes = apply_filters( 'woocommerce_admin_onboarding_themes', $themes );
		return array_values( $themes );
	}

	/**
	 * Get theme data used in onboarding theme browser.
	 *
	 * @param WP_Theme $theme Theme to gather data from.
	 * @return array
	 */
	public static function get_theme_data( $theme ) {
		return array(
			'slug'                    => sanitize_text_field( $theme->stylesheet ),
			'title'                   => $theme->get( 'Name' ),
			'price'                   => '0.00',
			'is_installed'            => true,
			'image'                   => $theme->get_screenshot(),
			'has_woocommerce_support' => true,
		);
	}

	/**
	 * Add theme data to response from themes controller.
	 *
	 * @param WP_REST_Response $response Rest response.
	 * @return WP_REST_Response
	 */
	public static function add_uploaded_theme_data( $response ) {
		if ( ! isset( $response->data['theme'] ) ) {
			return $response;
		}

		$theme                        = wp_get_theme( $response->data['theme'] );
		$response->data['theme_data'] = self::get_theme_data( $theme );

		return $response;
	}

	/**
	 * Delete the stored themes transient.
	 */
	public static function delete_themes_transient() {
		delete_transient( self::THEMES_TRANSIENT );
	}

	/**
	 * Add preloaded data to onboarding.
	 *
	 * @param array $settings Component settings.
	 *
	 * @return array
	 */
	public static function preload_data( $settings ) {
		$settings['onboarding']['activeTheme'] = get_option( 'stylesheet' );
		$settings['onboarding']['themes']      = self::get_themes();
		return $settings;
	}

	/**
	 * Gets an array of themes that can be installed & activated via the onboarding wizard.
	 *
	 * @return array
	 */
	public static function get_allowed_themes() {
		$allowed_themes = array();
		$themes         = self::get_themes();

		foreach ( $themes as $theme ) {
			$price = preg_replace( '/&#?[a-z0-9]+;/i', '', $theme['price'] );

			if ( $theme['is_installed'] || '0.00' === $price ) {
				$allowed_themes[] = $theme['slug'];
			}
		}

		return apply_filters( 'woocommerce_admin_onboarding_themes_whitelist', $allowed_themes );
	}

}
Internal/Admin/Onboarding/OnboardingJetpack.php000064400000003464152333011040015526 0ustar00<?php
/**
 * WooCommerce Onboarding Jetpack
 */

namespace Automattic\WooCommerce\Internal\Admin\Onboarding;

/**
 * Contains logic around Jetpack setup during onboarding.
 */
class OnboardingJetpack {
	/**
	 * Class instance.
	 *
	 * @var OnboardingJetpack instance
	 */
	private static $instance = null;

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Init.
	 */
	public function init() {
		add_action( 'woocommerce_admin_plugins_pre_activate', array( $this, 'activate_and_install_jetpack_ahead_of_wcpay' ) );
		add_action( 'woocommerce_admin_plugins_pre_install', array( $this, 'activate_and_install_jetpack_ahead_of_wcpay' ) );

		// Always hook into Jetpack connection even if outside of admin.
		add_action( 'jetpack_site_registered', array( $this, 'set_woocommerce_setup_jetpack_opted_in' ) );
	}

	/**
	 * Sets the woocommerce_setup_jetpack_opted_in to true when Jetpack connects to WPCOM.
	 */
	public function set_woocommerce_setup_jetpack_opted_in() {
		update_option( 'woocommerce_setup_jetpack_opted_in', true );
	}

	/**
	 * Ensure that Jetpack gets installed and activated ahead of WooCommerce Payments
	 * if both are being installed/activated at the same time.
	 *
	 * See: https://github.com/Automattic/woocommerce-payments/issues/1663
	 * See: https://github.com/Automattic/jetpack/issues/19624
	 *
	 * @param array $plugins A list of plugins to install or activate.
	 *
	 * @return array
	 */
	public function activate_and_install_jetpack_ahead_of_wcpay( $plugins ) {
		if ( in_array( 'jetpack', $plugins, true ) && in_array( 'woocommerce-payments', $plugins, true ) ) {
			array_unshift( $plugins, 'jetpack' );
			$plugins = array_unique( $plugins );
		}
		return $plugins;
	}

}
Internal/Admin/RemoteInboxNotifications.php000064400000001645152333011040015044 0ustar00<?php
/**
 * Remote Inbox Notifications feature.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\Features\Features;
use \Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsEngine;

/**
 * Remote Inbox Notifications feature logic.
 */
class RemoteInboxNotifications {
	/**
	 * Option name used to toggle this feature.
	 */
	const TOGGLE_OPTION_NAME = 'woocommerce_show_marketplace_suggestions';

	/**
	 * Class instance.
	 *
	 * @var RemoteInboxNotifications instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		if ( Features::is_enabled( 'remote-inbox-notifications' ) ) {
			RemoteInboxNotificationsEngine::init();
		}
	}
}
Internal/Admin/ActivityPanels.php000064400000003120152333011040013004 0ustar00<?php
/**
 * WooCommerce Activity Panel.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\Notes\Notes;

/**
 * Contains backend logic for the activity panel feature.
 */
class ActivityPanels {
	/**
	 * Class instance.
	 *
	 * @var ActivityPanels instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
		// Run after Automattic\WooCommerce\Internal\Admin\Loader.
		add_filter( 'woocommerce_components_settings', array( $this, 'component_settings' ), 20 );
		// New settings injection.
		add_filter( 'woocommerce_admin_shared_settings', array( $this, 'component_settings' ), 20 );
	}

	/**
	 * Adds fields so that we can store activity panel last read and open times.
	 *
	 * @param array $user_data_fields User data fields.
	 * @return array
	 */
	public function add_user_data_fields( $user_data_fields ) {
		return array_merge(
			$user_data_fields,
			array(
				'activity_panel_inbox_last_read',
				'activity_panel_reviews_last_read',
			)
		);
	}

	/**
	 * Add alert count to the component settings.
	 *
	 * @param array $settings Component settings.
	 */
	public function component_settings( $settings ) {
		$settings['alertCount'] = Notes::get_notes_count( array( 'error', 'update' ), array( 'unactioned' ) );
		return $settings;
	}
}
Internal/Admin/Analytics.php000064400000021702152333011040012002 0ustar00<?php
/**
 * WooCommerce Analytics.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\API\Reports\Cache;
use Automattic\WooCommerce\Admin\Features\Features;

/**
 * Contains backend logic for the Analytics feature.
 */
class Analytics {
	/**
	 * Option name used to toggle this feature.
	 */
	const TOGGLE_OPTION_NAME = 'woocommerce_analytics_enabled';
	/**
	 * Clear cache tool identifier.
	 */
	const CACHE_TOOL_ID = 'clear_woocommerce_analytics_cache';

	/**
	 * Class instance.
	 *
	 * @var Analytics instance
	 */
	protected static $instance = null;

	/**
	 * Determines if the feature has been toggled on or off.
	 *
	 * @var boolean
	 */
	protected static $is_updated = false;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		add_action( 'update_option_' . self::TOGGLE_OPTION_NAME, array( $this, 'reload_page_on_toggle' ), 10, 2 );
		add_action( 'woocommerce_settings_saved', array( $this, 'maybe_reload_page' ) );

		if ( ! Features::is_enabled( 'analytics' ) ) {
			return;
		}

		add_filter( 'woocommerce_component_settings_preload_endpoints', array( $this, 'add_preload_endpoints' ) );
		add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
		add_action( 'admin_menu', array( $this, 'register_pages' ) );
		add_filter( 'woocommerce_debug_tools', array( $this, 'register_cache_clear_tool' ) );
	}

	/**
	 * Add the feature toggle to the features settings.
	 *
	 * @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class).
	 *
	 * @param array $features Feature sections.
	 * @return array
	 */
	public static function add_feature_toggle( $features ) {
		return $features;
	}

	/**
	 * Reloads the page when the option is toggled to make sure all Analytics features are loaded.
	 *
	 * @param string $old_value Old value.
	 * @param string $value     New value.
	 */
	public static function reload_page_on_toggle( $old_value, $value ) {
		if ( $old_value === $value ) {
			return;
		}

		self::$is_updated = true;
	}

	/**
	 * Reload the page if the setting has been updated.
	 */
	public static function maybe_reload_page() {
		if ( ! isset( $_SERVER['REQUEST_URI'] ) || ! self::$is_updated ) {
			return;
		}

		wp_safe_redirect( wp_unslash( $_SERVER['REQUEST_URI'] ) );
		exit();
	}

	/**
	 * Preload data from the countries endpoint.
	 *
	 * @param array $endpoints Array of preloaded endpoints.
	 * @return array
	 */
	public function add_preload_endpoints( $endpoints ) {
		$endpoints['performanceIndicators'] = '/wc-analytics/reports/performance-indicators/allowed';
		$endpoints['leaderboards']          = '/wc-analytics/leaderboards/allowed';
		return $endpoints;
	}

	/**
	 * Adds fields so that we can store user preferences for the columns to display on a report.
	 *
	 * @param array $user_data_fields User data fields.
	 * @return array
	 */
	public function add_user_data_fields( $user_data_fields ) {
		return array_merge(
			$user_data_fields,
			array(
				'categories_report_columns',
				'coupons_report_columns',
				'customers_report_columns',
				'orders_report_columns',
				'products_report_columns',
				'revenue_report_columns',
				'taxes_report_columns',
				'variations_report_columns',
				'dashboard_sections',
				'dashboard_chart_type',
				'dashboard_chart_interval',
				'dashboard_leaderboard_rows',
			)
		);
	}

	/**
	 * Register the cache clearing tool on the WooCommerce > Status > Tools page.
	 *
	 * @param array $debug_tools Available debug tool registrations.
	 * @return array Filtered debug tool registrations.
	 */
	public function register_cache_clear_tool( $debug_tools ) {
		$settings_url = add_query_arg(
			array(
				'page' => 'wc-admin',
				'path' => '/analytics/settings',
			),
			get_admin_url( null, 'admin.php' )
		);

		$debug_tools[ self::CACHE_TOOL_ID ] = array(
			'name'     => __( 'Clear analytics cache', 'woocommerce' ),
			'button'   => __( 'Clear', 'woocommerce' ),
			'desc'     => sprintf(
				/* translators: 1: opening link tag, 2: closing tag */
				__( 'This tool will reset the cached values used in WooCommerce Analytics. If numbers still look off, try %1$sReimporting Historical Data%2$s.', 'woocommerce' ),
				'<a href="' . esc_url( $settings_url ) . '">',
				'</a>'
			),
			'callback' => array( $this, 'run_clear_cache_tool' ),
		);

		return $debug_tools;
	}

	/**
	 * Registers report pages.
	 */
	public function register_pages() {
		$report_pages = self::get_report_pages();
		foreach ( $report_pages as $report_page ) {
			if ( ! is_null( $report_page ) ) {
				wc_admin_register_page( $report_page );
			}
		}
	}

	/**
	 * Get report pages.
	 */
	public static function get_report_pages() {
		$overview_page = array(
			'id'       => 'woocommerce-analytics',
			'title'    => __( 'Analytics', 'woocommerce' ),
			'path'     => '/analytics/overview',
			'icon'     => 'dashicons-chart-bar',
			'position' => 57, // After WooCommerce & Product menu items.
		);

		$report_pages = array(
			$overview_page,
			array(
				'id'       => 'woocommerce-analytics-overview',
				'title'    => __( 'Overview', 'woocommerce' ),
				'parent'   => 'woocommerce-analytics',
				'path'     => '/analytics/overview',
				'nav_args' => array(
					'order'  => 10,
					'parent' => 'woocommerce-analytics',
				),
			),
			array(
				'id'       => 'woocommerce-analytics-products',
				'title'    => __( 'Products', 'woocommerce' ),
				'parent'   => 'woocommerce-analytics',
				'path'     => '/analytics/products',
				'nav_args' => array(
					'order'  => 20,
					'parent' => 'woocommerce-analytics',
				),
			),
			array(
				'id'       => 'woocommerce-analytics-revenue',
				'title'    => __( 'Revenue', 'woocommerce' ),
				'parent'   => 'woocommerce-analytics',
				'path'     => '/analytics/revenue',
				'nav_args' => array(
					'order'  => 30,
					'parent' => 'woocommerce-analytics',
				),
			),
			array(
				'id'       => 'woocommerce-analytics-orders',
				'title'    => __( 'Orders', 'woocommerce' ),
				'parent'   => 'woocommerce-analytics',
				'path'     => '/analytics/orders',
				'nav_args' => array(
					'order'  => 40,
					'parent' => 'woocommerce-analytics',
				),
			),
			array(
				'id'       => 'woocommerce-analytics-variations',
				'title'    => __( 'Variations', 'woocommerce' ),
				'parent'   => 'woocommerce-analytics',
				'path'     => '/analytics/variations',
				'nav_args' => array(
					'order'  => 50,
					'parent' => 'woocommerce-analytics',
				),
			),
			array(
				'id'       => 'woocommerce-analytics-categories',
				'title'    => __( 'Categories', 'woocommerce' ),
				'parent'   => 'woocommerce-analytics',
				'path'     => '/analytics/categories',
				'nav_args' => array(
					'order'  => 60,
					'parent' => 'woocommerce-analytics',
				),
			),
			array(
				'id'       => 'woocommerce-analytics-coupons',
				'title'    => __( 'Coupons', 'woocommerce' ),
				'parent'   => 'woocommerce-analytics',
				'path'     => '/analytics/coupons',
				'nav_args' => array(
					'order'  => 70,
					'parent' => 'woocommerce-analytics',
				),
			),
			array(
				'id'       => 'woocommerce-analytics-taxes',
				'title'    => __( 'Taxes', 'woocommerce' ),
				'parent'   => 'woocommerce-analytics',
				'path'     => '/analytics/taxes',
				'nav_args' => array(
					'order'  => 80,
					'parent' => 'woocommerce-analytics',
				),
			),
			array(
				'id'       => 'woocommerce-analytics-downloads',
				'title'    => __( 'Downloads', 'woocommerce' ),
				'parent'   => 'woocommerce-analytics',
				'path'     => '/analytics/downloads',
				'nav_args' => array(
					'order'  => 90,
					'parent' => 'woocommerce-analytics',
				),
			),
			'yes' === get_option( 'woocommerce_manage_stock' ) ? array(
				'id'       => 'woocommerce-analytics-stock',
				'title'    => __( 'Stock', 'woocommerce' ),
				'parent'   => 'woocommerce-analytics',
				'path'     => '/analytics/stock',
				'nav_args' => array(
					'order'  => 100,
					'parent' => 'woocommerce-analytics',
				),
			) : null,
			array(
				'id'     => 'woocommerce-analytics-customers',
				'title'  => __( 'Customers', 'woocommerce' ),
				'parent' => 'woocommerce',
				'path'   => '/customers',
			),
			array(
				'id'       => 'woocommerce-analytics-settings',
				'title'    => __( 'Settings', 'woocommerce' ),
				'parent'   => 'woocommerce-analytics',
				'path'     => '/analytics/settings',
				'nav_args' => array(
					'title'  => __( 'Analytics', 'woocommerce' ),
					'parent' => 'woocommerce-settings',
				),
			),
		);

		/**
		 * The analytics report items used in the menu.
		 *
		 * @since 6.4.0
		 */
		return apply_filters( 'woocommerce_analytics_report_menu_items', $report_pages );
	}

	/**
	 * "Clear" analytics cache by invalidating it.
	 */
	public function run_clear_cache_tool() {
		Cache::invalidate();

		return __( 'Analytics cache cleared.', 'woocommerce' );
	}
}
Internal/Admin/MobileAppBanner.php000064400000001674152333011040013057 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Admin;

defined( 'ABSPATH' ) || exit;

/**
 * Determine if the mobile app banner shows on Android devices
 */
class MobileAppBanner {
	/**
	 * Class instance.
	 *
	 * @var Analytics instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
	}

	/**
	 * Adds fields so that we can store user preferences for the mobile app banner
	 *
	 * @param array $user_data_fields User data fields.
	 * @return array
	 */
	public function add_user_data_fields( $user_data_fields ) {
		return array_merge(
			$user_data_fields,
			array(
				'android_app_banner_dismissed',
			)
		);
	}
}
Internal/Admin/ShippingLabelBanner.php000064400000010227152333011040013722 0ustar00<?php
/**
 * WooCommerce Shipping Label banner.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use \Automattic\Jetpack\Connection\Manager as Jetpack_Connection_Manager;

/**
 * Shows print shipping label banner on edit order page.
 */
class ShippingLabelBanner {

	/**
	 * Singleton for the display rules class
	 *
	 * @var ShippingLabelBannerDisplayRules
	 */
	private $shipping_label_banner_display_rules;

	/**
	 * Constructor
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}
		add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 6, 2 );
	}

	/**
	 * Check if WooCommerce Shipping makes sense for this merchant.
	 *
	 * @return bool
	 */
	private function should_show_meta_box() {
		if ( ! $this->shipping_label_banner_display_rules ) {
			$jetpack_version   = null;
			$jetpack_connected = null;
			$wcs_version       = null;
			$wcs_tos_accepted  = null;

			if ( defined( 'JETPACK__VERSION' ) ) {
				$jetpack_version = JETPACK__VERSION;
			}

			if ( class_exists( Jetpack_Connection_Manager::class ) ) {
				$jetpack_connected = ( new Jetpack_Connection_Manager() )->has_connected_owner();
			}

			if ( class_exists( '\WC_Connect_Loader' ) ) {
				$wcs_version = \WC_Connect_Loader::get_wcs_version();
			}

			if ( class_exists( '\WC_Connect_Options' ) ) {
				$wcs_tos_accepted = \WC_Connect_Options::get_option( 'tos_accepted' );
			}

			$incompatible_plugins = class_exists( '\WC_Shipping_Fedex_Init' ) ||
				class_exists( '\WC_Shipping_UPS_Init' ) ||
				class_exists( '\WC_Integration_ShippingEasy' ) ||
				class_exists( '\WC_ShipStation_Integration' );

			$this->shipping_label_banner_display_rules =
				new ShippingLabelBannerDisplayRules(
					$jetpack_version,
					$jetpack_connected,
					$wcs_version,
					$wcs_tos_accepted,
					$incompatible_plugins
				);
		}

		return $this->shipping_label_banner_display_rules->should_display_banner();
	}

	/**
	 * Add metabox to order page.
	 *
	 * @param string   $post_type current post type.
	 * @param \WP_Post $post Current post object.
	 */
	public function add_meta_boxes( $post_type, $post ) {
		if ( 'shop_order' !== $post_type ) {
			return;
		}
		$order = wc_get_order( $post );
		if ( $this->should_show_meta_box() ) {
			add_meta_box(
				'woocommerce-admin-print-label',
				__( 'Shipping Label', 'woocommerce' ),
				array( $this, 'meta_box' ),
				null,
				'normal',
				'high',
				array(
					'context' => 'shipping_label',
					'order'   => $post->ID,
					'items'   => $this->count_shippable_items( $order ),
				)
			);
			add_action( 'admin_enqueue_scripts', array( $this, 'add_print_shipping_label_script' ) );
		}
	}

	/**
	 * Count shippable items
	 *
	 * @param \WC_Order $order Current order.
	 * @return int
	 */
	private function count_shippable_items( \WC_Order $order ) {
		$count = 0;
		foreach ( $order->get_items() as $item ) {
			if ( $item instanceof \WC_Order_Item_Product ) {
				$product = $item->get_product();
				if ( $product && $product->needs_shipping() ) {
					$count += $item->get_quantity();
				}
			}
		}
		return $count;
	}
	/**
	 * Adds JS to order page to render shipping banner.
	 *
	 * @param string $hook current page hook.
	 */
	public function add_print_shipping_label_script( $hook ) {
		$rtl = is_rtl() ? '.rtl' : '';
		wp_enqueue_style(
			'print-shipping-label-banner-style',
			WCAdminAssets::get_url( "print-shipping-label-banner/style{$rtl}", 'css' ),
			array( 'wp-components' ),
			WCAdminAssets::get_file_version( 'css' )
		);

		WCAdminAssets::register_script( 'wp-admin-scripts', 'print-shipping-label-banner', true );

		$payload = array(
			'nonce'                 => wp_create_nonce( 'wp_rest' ),
			'baseURL'               => get_rest_url(),
			'wcs_server_connection' => true,
		);

		wp_localize_script( 'print-shipping-label-banner', 'wcConnectData', $payload );
	}

	/**
	 * Render placeholder metabox.
	 *
	 * @param \WP_Post $post current post.
	 * @param array    $args empty args.
	 */
	public function meta_box( $post, $args ) {

		?>
		<div id="wc-admin-shipping-banner-root" class="woocommerce <?php echo esc_attr( 'wc-admin-shipping-banner' ); ?>" data-args="<?php echo esc_attr( wp_json_encode( $args['args'] ) ); ?>">
		</div>
		<?php
	}
}
Internal/Admin/FeaturePlugin.php000064400000015347152333011040012635 0ustar00<?php
/**
 * WooCommerce Admin: Feature plugin main class.
 */

namespace Automattic\WooCommerce\Internal\Admin;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\API;
use \Automattic\WooCommerce\Admin\Notes\Notes;
use \Automattic\WooCommerce\Internal\Admin\Notes\OrderMilestones;
use \Automattic\WooCommerce\Internal\Admin\Notes\WooSubscriptionsNotes;
use \Automattic\WooCommerce\Internal\Admin\Notes\TrackingOptIn;
use \Automattic\WooCommerce\Internal\Admin\Notes\WooCommercePayments;
use \Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins;
use \Automattic\WooCommerce\Internal\Admin\Notes\TestCheckout;
use \Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses;
use \Automattic\WooCommerce\Internal\Admin\Notes\MerchantEmailNotifications;
use \Automattic\WooCommerce\Internal\Admin\Notes\WelcomeToWooCommerceForStoreUsers;
use \Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\PluginsHelper;
use Automattic\WooCommerce\Admin\PluginsInstaller;
use Automattic\WooCommerce\Admin\ReportExporter;
use Automattic\WooCommerce\Admin\ReportsSync;
use Automattic\WooCommerce\Internal\Admin\CategoryLookup;
use Automattic\WooCommerce\Internal\Admin\Events;
use Automattic\WooCommerce\Internal\Admin\Onboarding\Onboarding;

/**
 * Feature plugin main class.
 *
 * @internal This file will not be bundled with woo core, only the feature plugin.
 * @internal Note this is not called WC_Admin due to a class already existing in core with that name.
 */
class FeaturePlugin {
	/**
	 * The single instance of the class.
	 *
	 * @var object
	 */
	protected static $instance = null;

	/**
	 * Constructor
	 *
	 * @return void
	 */
	protected function __construct() {}

	/**
	 * Get class instance.
	 *
	 * @return object Instance.
	 */
	final public static function instance() {
		if ( null === static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Init the feature plugin, only if we can detect both Gutenberg and WooCommerce.
	 */
	public function init() {
		// Bail if WC isn't initialized (This can be called from WCAdmin's entrypoint).
		if ( ! defined( 'WC_ABSPATH' ) ) {
			return;
		}

		// Load the page controller functions file first to prevent fatal errors when disabling WooCommerce Admin.
		$this->define_constants();
		require_once WC_ADMIN_ABSPATH . '/includes/react-admin/page-controller-functions.php';
		require_once WC_ADMIN_ABSPATH . '/src/Admin/Notes/DeprecatedNotes.php';
		require_once WC_ADMIN_ABSPATH . '/includes/react-admin/core-functions.php';
		require_once WC_ADMIN_ABSPATH . '/includes/react-admin/feature-config.php';
		require_once WC_ADMIN_ABSPATH . '/includes/react-admin/wc-admin-update-functions.php';
		require_once WC_ADMIN_ABSPATH . '/includes/react-admin/class-experimental-abtest.php';

		if ( did_action( 'plugins_loaded' ) ) {
			self::on_plugins_loaded();
		} else {
			// Make sure we hook into `plugins_loaded` before core's Automattic\WooCommerce\Package::init().
			// If core is network activated but we aren't, the packaged version of WooCommerce Admin will
			// attempt to use a data store that hasn't been loaded yet - because we've defined our constants here.
			// See: https://github.com/woocommerce/woocommerce-admin/issues/3869.
			add_action( 'plugins_loaded', array( $this, 'on_plugins_loaded' ), 9 );
		}
	}

	/**
	 * Setup plugin once all other plugins are loaded.
	 *
	 * @return void
	 */
	public function on_plugins_loaded() {
		$this->hooks();
		$this->includes();
	}

	/**
	 * Define Constants.
	 */
	protected function define_constants() {
		$this->define( 'WC_ADMIN_APP', 'wc-admin-app' );
		$this->define( 'WC_ADMIN_ABSPATH', WC_ABSPATH );
		$this->define( 'WC_ADMIN_DIST_JS_FOLDER', 'assets/client/admin/' );
		$this->define( 'WC_ADMIN_DIST_CSS_FOLDER', 'assets/client/admin/' );
		$this->define( 'WC_ADMIN_PLUGIN_FILE', WC_PLUGIN_FILE );

		/**
		 * Define the WC Admin Images Folder URL.
		 *
		 * @deprecated 6.7.0
		 * @var string
		 */
		if ( ! defined( 'WC_ADMIN_IMAGES_FOLDER_URL' ) ) {
			/**
			 * Define the WC Admin Images Folder URL.
			 *
			 * @deprecated 6.7.0
			 * @var string
			 */
			define( 'WC_ADMIN_IMAGES_FOLDER_URL', plugins_url( 'assets/images', WC_PLUGIN_FILE ) );
		}

		/**
		 * Define the current WC Admin version.
		 *
		 * @deprecated 6.4.0
		 * @var string
		 */
		if ( ! defined( 'WC_ADMIN_VERSION_NUMBER' ) ) {
			/**
			  * Define the current WC Admin version.
			  *
			  * @deprecated 6.4.0
			  * @var string
			  */
			define( 'WC_ADMIN_VERSION_NUMBER', '3.3.0' );
		}
	}

	/**
	 * Include WC Admin classes.
	 */
	public function includes() {
		// Initialize Database updates, option migrations, and Notes.
		Events::instance()->init();
		Notes::init();

		// Initialize Plugins Installer.
		PluginsInstaller::init();
		PluginsHelper::init();

		// Initialize API.
		API\Init::instance();

		if ( Features::is_enabled( 'onboarding' ) ) {
			Onboarding::init();
		}

		if ( Features::is_enabled( 'analytics' ) ) {
			// Initialize Reports syncing.
			ReportsSync::init();
			CategoryLookup::instance()->init();

			// Initialize Reports exporter.
			ReportExporter::init();
		}

		// Admin note providers.
		// @todo These should be bundled in the features/ folder, but loading them from there currently has a load order issue.
		new WooSubscriptionsNotes();
		new OrderMilestones();
		new TrackingOptIn();
		new WooCommercePayments();
		new InstallJPAndWCSPlugins();
		new TestCheckout();
		new SellingOnlineCourses();
		new WelcomeToWooCommerceForStoreUsers();
		new MagentoMigration();

		// Initialize MerchantEmailNotifications.
		MerchantEmailNotifications::init();
	}

	/**
	 * Set up our admin hooks and plugin loader.
	 */
	protected function hooks() {
		add_filter( 'woocommerce_admin_features', array( $this, 'replace_supported_features' ), 0 );

		Loader::get_instance();
		WCAdminAssets::get_instance();
	}


	/**
	 * Overwrites the allowed features array using a local `feature-config.php` file.
	 *
	 * @param array $features Array of feature slugs.
	 */
	public function replace_supported_features( $features ) {
		/**
		 * Get additional feature config
		 *
		 * @since 6.5.0
		 */
		$feature_config = apply_filters( 'woocommerce_admin_get_feature_config', wc_admin_get_feature_config() );
		$features       = array_keys( array_filter( $feature_config ) );
		return $features;
	}

	/**
	 * Define constant if not already set.
	 *
	 * @param string      $name  Constant name.
	 * @param string|bool $value Constant value.
	 */
	protected function define( $name, $value ) {
		if ( ! defined( $name ) ) {
			define( $name, $value );
		}
	}

	/**
	 * Prevent cloning.
	 */
	private function __clone() {}

	/**
	 * Prevent unserializing.
	 */
	public function __wakeup() {
		die();
	}
}
Internal/Admin/Schedulers/OrdersScheduler.php000064400000020561152333011040015253 0ustar00<?php
/**
 * Order syncing related functions and actions.
 */

namespace Automattic\WooCommerce\Internal\Admin\Schedulers;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Taxes\DataStore as TaxesDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
use Automattic\WooCommerce\Admin\Overrides\Order;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Utilities\OrderUtil;

/**
 * OrdersScheduler Class.
 */
class OrdersScheduler extends ImportScheduler {
	/**
	 * Slug to identify the scheduler.
	 *
	 * @var string
	 */
	public static $name = 'orders';

	/**
	 * Attach order lookup update hooks.
	 *
	 * @internal
	 */
	public static function init() {
		// Activate WC_Order extension.
		\Automattic\WooCommerce\Admin\Overrides\Order::add_filters();
		\Automattic\WooCommerce\Admin\Overrides\OrderRefund::add_filters();

		// Order and refund data must be run on these hooks to ensure meta data is set.
		add_action( 'woocommerce_update_order', array( __CLASS__, 'possibly_schedule_import' ) );
		add_action( 'woocommerce_create_order', array( __CLASS__, 'possibly_schedule_import' ) );
		add_action( 'woocommerce_refund_created', array( __CLASS__, 'possibly_schedule_import' ) );

		OrdersStatsDataStore::init();
		CouponsDataStore::init();
		ProductsDataStore::init();
		TaxesDataStore::init();

		parent::init();
	}

	/**
	 * Add customer dependencies.
	 *
	 * @internal
	 * @return array
	 */
	public static function get_dependencies() {
		return array(
			'import_batch_init' => \Automattic\WooCommerce\Internal\Admin\Schedulers\CustomersScheduler::get_action( 'import_batch_init' ),
		);
	}

	/**
	 * Get the order/refund IDs and total count that need to be synced.
	 *
	 * @internal
	 * @param int      $limit Number of records to retrieve.
	 * @param int      $page  Page number.
	 * @param int|bool $days Number of days prior to current date to limit search results.
	 * @param bool     $skip_existing Skip already imported orders.
	 */
	public static function get_items( $limit = 10, $page = 1, $days = false, $skip_existing = false ) {
		if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
			return self::get_items_from_orders_table( $limit, $page, $days, $skip_existing );
		} else {
			return self::get_items_from_posts_table( $limit, $page, $days, $skip_existing );
		}
	}

	/**
	 * Helper method to ger order/refund IDS and total count that needs to be synced.
	 *
	 * @internal
	 * @param int      $limit Number of records to retrieve.
	 * @param int      $page  Page number.
	 * @param int|bool $days Number of days prior to current date to limit search results.
	 * @param bool     $skip_existing Skip already imported orders.
	 *
	 * @return object Total counts.
	 */
	private static function get_items_from_posts_table( $limit, $page, $days, $skip_existing ) {
		global $wpdb;
		$where_clause = '';
		$offset       = $page > 1 ? ( $page - 1 ) * $limit : 0;

		if ( is_int( $days ) ) {
			$days_ago      = gmdate( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) );
			$where_clause .= " AND post_date_gmt >= '{$days_ago}'";
		}

		if ( $skip_existing ) {
			$where_clause .= " AND NOT EXISTS (
				SELECT 1 FROM {$wpdb->prefix}wc_order_stats
				WHERE {$wpdb->prefix}wc_order_stats.order_id = {$wpdb->posts}.ID
			)";
		}

		$count = $wpdb->get_var(
			"SELECT COUNT(*) FROM {$wpdb->posts}
			WHERE post_type IN ( 'shop_order', 'shop_order_refund' )
			AND post_status NOT IN ( 'wc-auto-draft', 'auto-draft', 'trash' )
			{$where_clause}"
		); // phpcs:ignore unprepared SQL ok.

		$order_ids = absint( $count ) > 0 ? $wpdb->get_col(
			$wpdb->prepare(
				"SELECT ID FROM {$wpdb->posts}
				WHERE post_type IN ( 'shop_order', 'shop_order_refund' )
				AND post_status NOT IN ( 'wc-auto-draft', 'auto-draft', 'trash' )
				{$where_clause}
				ORDER BY post_date_gmt ASC
				LIMIT %d
				OFFSET %d",
				$limit,
				$offset
			)
		) : array(); // phpcs:ignore unprepared SQL ok.

		return (object) array(
			'total' => absint( $count ),
			'ids'   => $order_ids,
		);
	}

	/**
	 * Helper method to ger order/refund IDS and total count that needs to be synced from HPOS.
	 *
	 * @internal
	 * @param int      $limit Number of records to retrieve.
	 * @param int      $page  Page number.
	 * @param int|bool $days Number of days prior to current date to limit search results.
	 * @param bool     $skip_existing Skip already imported orders.
	 *
	 * @return object Total counts.
	 */
	private static function get_items_from_orders_table( $limit, $page, $days, $skip_existing ) {
		global $wpdb;
		$where_clause = '';
		$offset       = $page > 1 ? ( $page - 1 ) * $limit : 0;
		$order_table  = OrdersTableDataStore::get_orders_table_name();

		if ( is_int( $days ) ) {
			$days_ago      = gmdate( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) );
			$where_clause .= " AND orders.date_created_gmt >= '{$days_ago}'";
		}

		if ( $skip_existing ) {
			$where_clause .= "AND NOT EXiSTS (
					SELECT 1 FROM {$wpdb->prefix}wc_order_stats
					WHERE {$wpdb->prefix}wc_order_stats.order_id = orders.id
					)
				";
		}

		$count = $wpdb->get_var(
			"
SELECT COUNT(*) FROM {$order_table} AS orders
WHERE type in ( 'shop_order', 'shop_order_refund' )
AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
{$where_clause}
"
		); // phpcs:ignore unprepared SQL ok.

		$order_ids = absint( $count ) > 0 ? $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM {$order_table} AS orders
				WHERE type IN ( 'shop_order', 'shop_order_refund' )
				AND status NOT IN ( 'wc-auto-draft', 'auto-draft', 'trash' )
				{$where_clause}
				ORDER BY date_created_gmt ASC
				LIMIT %d
				OFFSET %d",
				$limit,
				$offset
			)
		) : array(); // phpcs:ignore unprepared SQL ok.

		return (object) array(
			'total' => absint( $count ),
			'ids'   => $order_ids,
		);
	}

	/**
	 * Get total number of rows imported.
	 *
	 * @internal
	 */
	public static function get_total_imported() {
		global $wpdb;
		return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_order_stats" );
	}

	/**
	 * Schedule this import if the post is an order or refund.
	 *
	 * @param int $order_id Post ID.
	 *
	 * @internal
	 */
	public static function possibly_schedule_import( $order_id ) {
		if ( ! OrderUtil::is_order( $order_id, array( 'shop_order' ) ) && 'woocommerce_refund_created' !== current_filter() ) {
			return;
		}

		self::schedule_action( 'import', array( $order_id ) );
	}

	/**
	 * Imports a single order or refund to update lookup tables for.
	 * If an error is encountered in one of the updates, a retry action is scheduled.
	 *
	 * @internal
	 * @param int $order_id Order or refund ID.
	 * @return void
	 */
	public static function import( $order_id ) {
		$order = wc_get_order( $order_id );

		// If the order isn't found for some reason, skip the sync.
		if ( ! $order ) {
			return;
		}

		$type = $order->get_type();

		// If the order isn't the right type, skip sync.
		if ( 'shop_order' !== $type && 'shop_order_refund' !== $type ) {
			return;
		}

		// If the order has no id or date created, skip sync.
		if ( ! $order->get_id() || ! $order->get_date_created() ) {
			return;
		}

		$results = array(
			OrdersStatsDataStore::sync_order( $order_id ),
			ProductsDataStore::sync_order_products( $order_id ),
			CouponsDataStore::sync_order_coupons( $order_id ),
			TaxesDataStore::sync_order_taxes( $order_id ),
			CustomersDataStore::sync_order_customer( $order_id ),
		);

		if ( 'shop_order' === $type ) {
			$order_refunds = $order->get_refunds();

			foreach ( $order_refunds as $refund ) {
				OrdersStatsDataStore::sync_order( $refund->get_id() );
			}
		}

		ReportsCache::invalidate();
	}

	/**
	 * Delete a batch of orders.
	 *
	 * @internal
	 * @param int $batch_size Number of items to delete.
	 * @return void
	 */
	public static function delete( $batch_size ) {
		global $wpdb;

		$order_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT order_id FROM {$wpdb->prefix}wc_order_stats ORDER BY order_id ASC LIMIT %d",
				$batch_size
			)
		);

		foreach ( $order_ids as $order_id ) {
			OrdersStatsDataStore::delete_order( $order_id );
		}
	}
}
Internal/Admin/Schedulers/ImportScheduler.php000064400000011461152333011040015266 0ustar00<?php
/**
 * Import related functions and actions.
 */

namespace Automattic\WooCommerce\Internal\Admin\Schedulers;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
use \Automattic\WooCommerce\Admin\Schedulers\SchedulerTraits;

/**
 * ImportScheduler class.
 */
abstract class ImportScheduler implements ImportInterface {
	/**
	 * Import stats option name.
	 */
	const IMPORT_STATS_OPTION = 'woocommerce_admin_import_stats';

	/**
	 * Scheduler traits.
	 */
	use SchedulerTraits {
		get_batch_sizes as get_scheduler_batch_sizes;
	}

	/**
	 * Returns true if an import is in progress.
	 *
	 * @internal
	 * @return bool
	 */
	public static function is_importing() {
		$pending_jobs = self::queue()->search(
			array(
				'status'   => 'pending',
				'per_page' => 1,
				'claimed'  => false,
				'search'   => 'import',
				'group'    => self::$group,
			)
		);
		if ( empty( $pending_jobs ) ) {
			$in_progress = self::queue()->search(
				array(
					'status'   => 'in-progress',
					'per_page' => 1,
					'search'   => 'import',
					'group'    => self::$group,
				)
			);
		}

		return ! empty( $pending_jobs ) || ! empty( $in_progress );
	}

	/**
	 * Get batch sizes.
	 *
	 * @internal
	 * @retun array
	 */
	public static function get_batch_sizes() {
		return array_merge(
			self::get_scheduler_batch_sizes(),
			array(
				'delete' => 10,
				'import' => 25,
				'queue'  => 100,
			)
		);

	}

	/**
	 * Get all available scheduling actions.
	 * Used to determine action hook names and clear events.
	 *
	 * @internal
	 * @return array
	 */
	public static function get_scheduler_actions() {
		return array(
			'import_batch_init' => 'wc-admin_import_batch_init_' . static::$name,
			'import_batch'      => 'wc-admin_import_batch_' . static::$name,
			'delete_batch_init' => 'wc-admin_delete_batch_init_' . static::$name,
			'delete_batch'      => 'wc-admin_delete_batch_' . static::$name,
			'import'            => 'wc-admin_import_' . static::$name,
		);
	}

	/**
	 * Queue the imports into multiple batches.
	 *
	 * @internal
	 * @param integer|boolean $days Number of days to import.
	 * @param boolean         $skip_existing Skip exisiting records.
	 */
	public static function import_batch_init( $days, $skip_existing ) {
		$batch_size = static::get_batch_size( 'import' );
		$items      = static::get_items( 1, 1, $days, $skip_existing );

		if ( 0 === $items->total ) {
			return;
		}

		$num_batches = ceil( $items->total / $batch_size );

		self::queue_batches( 1, $num_batches, 'import_batch', array( $days, $skip_existing ) );
	}

	/**
	 * Imports a batch of items to update.
	 *
	 * @internal
	 * @param int      $batch_number Batch number to import (essentially a query page number).
	 * @param int|bool $days Number of days to import.
	 * @param bool     $skip_existing Skip exisiting records.
	 * @return void
	 */
	public static function import_batch( $batch_number, $days, $skip_existing ) {
		$batch_size = static::get_batch_size( 'import' );

		$properties = array(
			'batch_number' => $batch_number,
			'batch_size'   => $batch_size,
			'type'         => static::$name,
		);
		wc_admin_record_tracks_event( 'import_job_start', $properties );

		// When we are skipping already imported items, the table of items to import gets smaller in
		// every batch, so we want to always import the first page.
		$page  = $skip_existing ? 1 : $batch_number;
		$items = static::get_items( $batch_size, $page, $days, $skip_existing );

		foreach ( $items->ids as $id ) {
			static::import( $id );
		}

		$import_stats                              = get_option( self::IMPORT_STATS_OPTION, array() );
		$imported_count                            = absint( $import_stats[ static::$name ]['imported'] ) + count( $items->ids );
		$import_stats[ static::$name ]['imported'] = $imported_count;
		update_option( self::IMPORT_STATS_OPTION, $import_stats );

		$properties['imported_count'] = $imported_count;

		wc_admin_record_tracks_event( 'import_job_complete', $properties );
	}

	/**
	 * Queue item deletion in batches.
	 *
	 * @internal
	 */
	public static function delete_batch_init() {
		global $wpdb;
		$batch_size = static::get_batch_size( 'delete' );
		$count      = static::get_total_imported();

		if ( 0 === $count ) {
			return;
		}

		$num_batches = ceil( $count / $batch_size );

		self::queue_batches( 1, $num_batches, 'delete_batch' );
	}

	/**
	 * Delete a batch by passing the count to be deleted to the child delete method.
	 *
	 * @internal
	 * @return void
	 */
	public static function delete_batch() {
		wc_admin_record_tracks_event( 'delete_import_data_job_start', array( 'type' => static::$name ) );

		$batch_size = static::get_batch_size( 'delete' );
		static::delete( $batch_size );

		ReportsCache::invalidate();

		wc_admin_record_tracks_event( 'delete_import_data_job_complete', array( 'type' => static::$name ) );
	}
}
Internal/Admin/Schedulers/MailchimpScheduler.php000064400000007160152333011040015720 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Admin\Schedulers;

/**
 * Class MailchimpScheduler
 *
 * @package Automattic\WooCommerce\Admin\Schedulers
 */
class MailchimpScheduler {

	const SUBSCRIBE_ENDPOINT     = 'https://woocommerce.com/wp-json/wccom/v1/subscribe';
	const SUBSCRIBE_ENDPOINT_DEV = 'http://woocommerce.test/wp-json/wccom/v1/subscribe';

	const SUBSCRIBED_OPTION_NAME             = 'woocommerce_onboarding_subscribed_to_mailchimp';
	const SUBSCRIBED_ERROR_COUNT_OPTION_NAME = 'woocommerce_onboarding_subscribed_to_mailchimp_error_count';
	const MAX_ERROR_THRESHOLD                = 3;

	const LOGGER_CONTEXT = 'mailchimp_scheduler';

	/**
	 * The logger instance.
	 *
	 * @var \WC_Logger_Interface|null
	 */
	private $logger;

	/**
	 * MailchimpScheduler constructor.
	 *
	 * @internal
	 * @param \WC_Logger_Interface|null $logger Logger instance.
	 */
	public function __construct( \WC_Logger_Interface $logger = null ) {
		if ( null === $logger ) {
			$logger = wc_get_logger();
		}
		$this->logger = $logger;
	}

	/**
	 * Attempt to subscribe store_email to MailChimp.
	 *
	 * @internal
	 */
	public function run() {
		// Abort if we've already subscribed to MailChimp.
		if ( 'yes' === get_option( self::SUBSCRIBED_OPTION_NAME ) ) {
			return false;
		}

		$profile_data = get_option( 'woocommerce_onboarding_profile' );
		if ( ! isset( $profile_data['is_agree_marketing'] ) || false === $profile_data['is_agree_marketing'] ) {
			return false;
		}

		// Abort if store_email doesn't exist.
		if ( ! isset( $profile_data['store_email'] ) ) {
			return false;
		}

		// Abort if failed requests reaches the threshold.
		if ( intval( get_option( self::SUBSCRIBED_ERROR_COUNT_OPTION_NAME, 0 ) ) >= self::MAX_ERROR_THRESHOLD ) {
			return false;
		}

		$response = $this->make_request( $profile_data['store_email'] );
		if ( is_wp_error( $response ) || ! isset( $response['body'] ) ) {
			$this->handle_request_error();
			return false;
		}

		$body = json_decode( $response['body'] );
		if ( isset( $body->success ) && true === $body->success ) {
			update_option( self::SUBSCRIBED_OPTION_NAME, 'yes' );
			return true;
		}

		$this->handle_request_error( $body );
		return false;
	}

	/**
	 * Make an HTTP request to the API.
	 *
	 * @internal
	 * @param string $store_email Email address to subscribe.
	 *
	 * @return mixed
	 */
	public function make_request( $store_email ) {
		if ( true === defined( 'WP_ENVIRONMENT_TYPE' ) && 'development' === constant( 'WP_ENVIRONMENT_TYPE' ) ) {
			$subscribe_endpoint = self::SUBSCRIBE_ENDPOINT_DEV;
		} else {
			$subscribe_endpoint = self::SUBSCRIBE_ENDPOINT;
		}

		return wp_remote_post(
			$subscribe_endpoint,
			array(
				'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
				'method'     => 'POST',
				'body'       => array(
					'email' => $store_email,
				),
			)
		);
	}

	/**
	 * Reset options.
	 *
	 * @internal
	 */
	public static function reset() {
		delete_option( self::SUBSCRIBED_OPTION_NAME );
		delete_option( self::SUBSCRIBED_ERROR_COUNT_OPTION_NAME );
	}

	/**
	 * Handle subscribe API error.
	 *
	 * @internal
	 * @param string $extra_msg  Extra message to log.
	 */
	private function handle_request_error( $extra_msg = null ) {
		// phpcs:ignore
		$msg = isset( $extra_msg ) ? 'Incorrect response from Mailchimp API with: ' . print_r( $extra_msg, true ) : 'Error getting a response from Mailchimp API.';

		$this->logger->error( $msg, array( 'source' => self::LOGGER_CONTEXT ) );

		$accumulated_error_count = intval( get_option( self::SUBSCRIBED_ERROR_COUNT_OPTION_NAME, 0 ) ) + 1;
		update_option( self::SUBSCRIBED_ERROR_COUNT_OPTION_NAME, $accumulated_error_count );
	}
}
Internal/Admin/Schedulers/ImportInterface.php000064400000001306152333011040015245 0ustar00<?php
/**
 * Import related abstract functions.
 */

namespace Automattic\WooCommerce\Internal\Admin\Schedulers;

interface ImportInterface {
	/**
	 * Get items based on query and return IDs along with total available.
	 *
	 * @internal
	 * @param int      $limit Number of records to retrieve.
	 * @param int      $page  Page number.
	 * @param int|bool $days Number of days prior to current date to limit search results.
	 * @param bool     $skip_existing Skip already imported items.
	 */
	public static function get_items( $limit, $page, $days, $skip_existing );

	/**
	 * Get total number of items already imported.
	 *
	 * @internal
	 * @return null
	 */
	public static function get_total_imported();

}
Internal/Admin/Schedulers/CustomersScheduler.php000064400000016573152333011040016011 0ustar00<?php
/**
 * Customer syncing related functions and actions.
 */

namespace Automattic\WooCommerce\Internal\Admin\Schedulers;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;

/**
 * CustomersScheduler Class.
 */
class CustomersScheduler extends ImportScheduler {
	/**
	 * Slug to identify the scheduler.
	 *
	 * @var string
	 */
	public static $name = 'customers';

	/**
	 * Attach customer lookup update hooks.
	 *
	 * @internal
	 */
	public static function init() {
		add_action( 'woocommerce_new_customer', array( __CLASS__, 'schedule_import' ) );
		add_action( 'woocommerce_update_customer', array( __CLASS__, 'schedule_import' ) );
		add_action( 'updated_user_meta', array( __CLASS__, 'schedule_import_via_last_active' ), 10, 3 );
		add_action( 'woocommerce_privacy_remove_order_personal_data', array( __CLASS__, 'schedule_anonymize' ) );
		add_action( 'delete_user', array( __CLASS__, 'schedule_user_delete' ) );
		add_action( 'remove_user_from_blog', array( __CLASS__, 'schedule_user_delete' ) );

		CustomersDataStore::init();
		parent::init();
	}

	/**
	 * Add customer dependencies.
	 *
	 * @internal
	 * @return array
	 */
	public static function get_dependencies() {
		return array(
			'delete_batch_init' => OrdersScheduler::get_action( 'delete_batch_init' ),
			'anonymize'         => self::get_action( 'import' ),
			'delete_user'       => self::get_action( 'import' ),
		);
	}

	/**
	 * Get the customer IDs and total count that need to be synced.
	 *
	 * @internal
	 * @param int      $limit Number of records to retrieve.
	 * @param int      $page  Page number.
	 * @param int|bool $days Number of days prior to current date to limit search results.
	 * @param bool     $skip_existing Skip already imported customers.
	 */
	public static function get_items( $limit = 10, $page = 1, $days = false, $skip_existing = false ) {
		$customer_roles = apply_filters( 'woocommerce_analytics_import_customer_roles', array( 'customer' ) );
		$query_args     = array(
			'fields'   => 'ID',
			'orderby'  => 'ID',
			'order'    => 'ASC',
			'number'   => $limit,
			'paged'    => $page,
			'role__in' => $customer_roles,
		);

		if ( is_int( $days ) ) {
			$query_args['date_query'] = array(
				'after' => gmdate( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) ),
			);
		}

		if ( $skip_existing ) {
			add_action( 'pre_user_query', array( __CLASS__, 'exclude_existing_customers_from_query' ) );
		}

		$customer_query = new \WP_User_Query( $query_args );

		remove_action( 'pre_user_query', array( __CLASS__, 'exclude_existing_customers_from_query' ) );

		return (object) array(
			'total' => $customer_query->get_total(),
			'ids'   => $customer_query->get_results(),
		);
	}

	/**
	 * Exclude users that already exist in our customer lookup table.
	 *
	 * Meant to be hooked into 'pre_user_query' action.
	 *
	 * @internal
	 * @param WP_User_Query $wp_user_query WP_User_Query to modify.
	 */
	public static function exclude_existing_customers_from_query( $wp_user_query ) {
		global $wpdb;

		$wp_user_query->query_where .= " AND NOT EXISTS (
			SELECT ID FROM {$wpdb->prefix}wc_customer_lookup
			WHERE {$wpdb->prefix}wc_customer_lookup.user_id = {$wpdb->users}.ID
		)";
	}

	/**
	 * Get total number of rows imported.
	 *
	 * @internal
	 * @return int
	 */
	public static function get_total_imported() {
		global $wpdb;
		return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_customer_lookup" );
	}

	/**
	 * Get all available scheduling actions.
	 * Used to determine action hook names and clear events.
	 *
	 * @internal
	 * @return array
	 */
	public static function get_scheduler_actions() {
		$actions                = parent::get_scheduler_actions();
		$actions['anonymize']   = 'wc-admin_anonymize_' . static::$name;
		$actions['delete_user'] = 'wc-admin_delete_user_' . static::$name;
		return $actions;
	}

	/**
	 * Schedule import.
	 *
	 * @internal
	 * @param int $user_id User ID.
	 * @return void
	 */
	public static function schedule_import( $user_id ) {
		self::schedule_action( 'import', array( $user_id ) );
	}

	/**
	 * Schedule an import if the "last active" meta value was changed.
	 * Function expects to be hooked into the `updated_user_meta` action.
	 *
	 * @internal
	 * @param int    $meta_id ID of updated metadata entry.
	 * @param int    $user_id ID of the user being updated.
	 * @param string $meta_key Meta key being updated.
	 */
	public static function schedule_import_via_last_active( $meta_id, $user_id, $meta_key ) {
		if ( 'wc_last_active' === $meta_key ) {
			self::schedule_import( $user_id );
		}
	}

	/**
	 * Schedule an action to anonymize a single Order.
	 *
	 * @internal
	 * @param WC_Order $order Order object.
	 * @return void
	 */
	public static function schedule_anonymize( $order ) {
		if ( is_a( $order, 'WC_Order' ) ) {
			// Postpone until any pending updates are completed.
			self::schedule_action( 'anonymize', array( $order->get_id() ) );
		}
	}

	/**
	 * Schedule an action to delete a single User.
	 *
	 * @internal
	 * @param int $user_id User ID.
	 * @return void
	 */
	public static function schedule_user_delete( $user_id ) {
		if ( (int) $user_id > 0 && ! doing_action( 'wp_uninitialize_site' ) ) {
			// Postpone until any pending updates are completed.
			self::schedule_action( 'delete_user', array( $user_id ) );
		}
	}

	/**
	 * Imports a single customer.
	 *
	 * @internal
	 * @param int $user_id User ID.
	 * @return void
	 */
	public static function import( $user_id ) {
		CustomersDataStore::update_registered_customer( $user_id );
	}

	/**
	 * Delete a batch of customers.
	 *
	 * @internal
	 * @param int $batch_size Number of items to delete.
	 * @return void
	 */
	public static function delete( $batch_size ) {
		global $wpdb;

		$customer_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT customer_id FROM {$wpdb->prefix}wc_customer_lookup ORDER BY customer_id ASC LIMIT %d",
				$batch_size
			)
		);

		foreach ( $customer_ids as $customer_id ) {
			CustomersDataStore::delete_customer( $customer_id );
		}
	}

	/**
	 * Anonymize the customer data for a single order.
	 *
	 * @internal
	 * @param int $order_id Order id.
	 * @return void
	 */
	public static function anonymize( $order_id ) {
		global $wpdb;

		$customer_id = $wpdb->get_var(
			$wpdb->prepare( "SELECT customer_id FROM {$wpdb->prefix}wc_order_stats WHERE order_id = %d", $order_id )
		);

		if ( ! $customer_id ) {
			return;
		}

		// Long form query because $wpdb->update rejects [deleted].
		$deleted_text = __( '[deleted]', 'woocommerce' );
		$updated      = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->prefix}wc_customer_lookup
					SET
						user_id = NULL,
						username = %s,
						first_name = %s,
						last_name = %s,
						email = %s,
						country = '',
						postcode = %s,
						city = %s,
						state = %s
					WHERE
						customer_id = %d",
				array(
					$deleted_text,
					$deleted_text,
					$deleted_text,
					'deleted@site.invalid',
					$deleted_text,
					$deleted_text,
					$deleted_text,
					$customer_id,
				)
			)
		);
		// If the customer row was anonymized, flush the cache.
		if ( $updated ) {
			ReportsCache::invalidate();
		}
	}

	/**
	 * Delete the customer data for a single user.
	 *
	 * @internal
	 * @param int $user_id User ID.
	 * @return void
	 */
	public static function delete_user( $user_id ) {
		CustomersDataStore::delete_customer_by_user_id( $user_id );
	}
}
Internal/Admin/Loader.php000064400000047441152333011040011271 0ustar00<?php
/**
 * Register the scripts, styles, and includes needed for pieces of the WooCommerce Admin experience.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Admin\PluginsHelper;
use Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews;
use Automattic\WooCommerce\Internal\Admin\ProductReviews\ReviewsCommentsOverrides;
use Automattic\WooCommerce\Internal\Admin\Settings;

/**
 * Loader Class.
 */
class Loader {
	/**
	 * Class instance.
	 *
	 * @var Loader instance
	 */
	protected static $instance = null;

	/**
	 * An array of classes to load from the includes folder.
	 *
	 * @var array
	 */
	protected static $classes = array();

	/**
	 * WordPress capability required to use analytics features.
	 *
	 * @var string
	 */
	protected static $required_capability = null;

	/**
	 * An array of dependencies that have been preloaded (to avoid duplicates).
	 *
	 * @var array
	 */
	protected $preloaded_dependencies = array(
		'script' => array(),
		'style'  => array(),
	);

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor.
	 * Hooks added here should be removed in `wc_admin_initialize` via the feature plugin.
	 */
	public function __construct() {
		Features::get_instance();
		WCAdminSharedSettings::get_instance();
		Translations::get_instance();
		WCAdminUser::get_instance();
		Settings::get_instance();
		SystemStatusReport::get_instance();

		wc_get_container()->get( Reviews::class );
		wc_get_container()->get( ReviewsCommentsOverrides::class );

		add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_classes' ) );
		add_filter( 'admin_title', array( __CLASS__, 'update_admin_title' ) );
		add_action( 'in_admin_header', array( __CLASS__, 'embed_page_header' ) );
		add_action( 'admin_head', array( __CLASS__, 'remove_notices' ) );
		add_action( 'admin_head', array( __CLASS__, 'smart_app_banner' ) );
		add_action( 'admin_notices', array( __CLASS__, 'inject_before_notices' ), -9999 );
		add_action( 'admin_notices', array( __CLASS__, 'inject_after_notices' ), PHP_INT_MAX );

		// Added this hook to delete the field woocommerce_onboarding_homepage_post_id when deleting the homepage.
		add_action( 'trashed_post', array( __CLASS__, 'delete_homepage' ) );

		/*
		* Remove the emoji script as it always defaults to replacing emojis with Twemoji images.
		* Gutenberg has also disabled emojis. More on that here -> https://github.com/WordPress/gutenberg/pull/6151
		*/
		remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );

		add_action( 'admin_init', array( __CLASS__, 'deactivate_wc_admin_plugin' ) );
	}

	/**
	 * If WooCommerce Admin is installed and activated, it will attempt to deactivate and show a notice.
	 */
	public static function deactivate_wc_admin_plugin() {
		$plugin_path = PluginsHelper::get_plugin_path_from_slug( 'woocommerce-admin' );
		if ( is_plugin_active( $plugin_path ) ) {
			$path = PluginsHelper::get_plugin_path_from_slug( 'woocommerce-admin' );
			deactivate_plugins( $path );
			$notice_action = is_network_admin() ? 'network_admin_notices' : 'admin_notices';
			add_action(
				$notice_action,
				function() {
					echo '<div class="error"><p>';
					printf(
						/* translators: %s: is referring to the plugin's name. */
						esc_html__( 'The %1$s plugin has been deactivated as the latest improvements are now included with the %2$s plugin.', 'woocommerce' ),
						'<code>WooCommerce Admin</code>',
						'<code>WooCommerce</code>'
					);
					echo '</p></div>';
				}
			);
		}
	}

	/**
	 * Returns breadcrumbs for the current page.
	 */
	private static function get_embed_breadcrumbs() {
		return wc_admin_get_breadcrumbs();
	}

	/**
	 * Outputs breadcrumbs via PHP for the initial load of an embedded page.
	 *
	 * @param array $section Section to create breadcrumb from.
	 */
	private static function output_heading( $section ) {
		echo esc_html( $section );
	}

	/**
	 * Set up a div for the header embed to render into.
	 * The initial contents here are meant as a place loader for when the PHP page initialy loads.
	 */
	public static function embed_page_header() {
		if ( ! PageController::is_admin_page() && ! PageController::is_embed_page() ) {
			return;
		}

		if ( ! PageController::is_embed_page() ) {
			return;
		}

		$sections = self::get_embed_breadcrumbs();
		$sections = is_array( $sections ) ? $sections : array( $sections );
		?>
		<div id="woocommerce-embedded-root" class="is-embed-loading">
			<div class="woocommerce-layout">
				<div class="woocommerce-layout__header is-embed-loading">
					<h1 class="woocommerce-layout__header-heading">
						<?php self::output_heading( end( $sections ) ); ?>
					</h1>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Adds body classes to the main wp-admin wrapper, allowing us to better target elements in specific scenarios.
	 *
	 * @param string $admin_body_class Body class to add.
	 */
	public static function add_admin_body_classes( $admin_body_class = '' ) {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return $admin_body_class;
		}

		$classes   = explode( ' ', trim( $admin_body_class ) );
		$classes[] = 'woocommerce-page';
		if ( PageController::is_embed_page() ) {
			$classes[] = 'woocommerce-embed-page';
		}

		/**
		 * Some routes or features like onboarding hide the wp-admin navigation and masterbar.
		 * Setting `woocommerce_admin_is_loading` to true allows us to premeptively hide these
		 * elements while the JS app loads.
		 * This class needs to be removed by those feature components (like <ProfileWizard />).
		 *
		 * @param bool $is_loading If WooCommerce Admin is loading a fullscreen view.
		 */
		$is_loading = apply_filters( 'woocommerce_admin_is_loading', false );

		if ( PageController::is_admin_page() && $is_loading ) {
			$classes[] = 'woocommerce-admin-is-loading';
		}

		$admin_body_class = implode( ' ', array_unique( $classes ) );
		return " $admin_body_class ";
	}

	/**
	 * Adds an iOS "Smart App Banner" for display on iOS Safari.
	 * See https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html
	 */
	public static function smart_app_banner() {
		if ( PageController::is_admin_or_embed_page() ) {
			echo "
				<meta name='apple-itunes-app' content='app-id=1389130815'>
			";
		}
	}


	/**
	 * Removes notices that should not be displayed on WC Admin pages.
	 */
	public static function remove_notices() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}

		// Hello Dolly.
		if ( function_exists( 'hello_dolly' ) ) {
			remove_action( 'admin_notices', 'hello_dolly' );
		}
	}

	/**
	 * Runs before admin notices action and hides them.
	 */
	public static function inject_before_notices() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}

		// The JITMs won't be shown in the Onboarding Wizard.
		$is_onboarding   = isset( $_GET['path'] ) && '/setup-wizard' === wc_clean( wp_unslash( $_GET['path'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
		$maybe_hide_jitm = $is_onboarding ? '-hide' : '';

		echo '<div class="woocommerce-layout__jitm' . sanitize_html_class( $maybe_hide_jitm ) . '" id="jp-admin-notices"></div>';

		// Wrap the notices in a hidden div to prevent flickering before
		// they are moved elsewhere in the page by WordPress Core.
		echo '<div class="woocommerce-layout__notice-list-hide" id="wp__notice-list">';

		if ( PageController::is_admin_page() ) {
			// Capture all notices and hide them. WordPress Core looks for
			// `.wp-header-end` and appends notices after it if found.
			// https://github.com/WordPress/WordPress/blob/f6a37e7d39e2534d05b9e542045174498edfe536/wp-admin/js/common.js#L737 .
			echo '<div class="wp-header-end" id="woocommerce-layout__notice-catcher"></div>';
		}
	}

	/**
	 * Runs after admin notices and closes div.
	 */
	public static function inject_after_notices() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}

		// Close the hidden div used to prevent notices from flickering before
		// they are inserted elsewhere in the page.
		echo '</div>';
	}

	/**
	 * Edits Admin title based on section of wc-admin.
	 *
	 * @param string $admin_title Modifies admin title.
	 * @todo Can we do some URL rewriting so we can figure out which page they are on server side?
	 */
	public static function update_admin_title( $admin_title ) {
		if (
			! did_action( 'current_screen' ) ||
			! PageController::is_admin_page()
		) {
			return $admin_title;
		}

		$sections = self::get_embed_breadcrumbs();
		$pieces   = array();

		foreach ( $sections as $section ) {
			$pieces[] = is_array( $section ) ? $section[1] : $section;
		}

		$pieces = array_reverse( $pieces );
		$title  = implode( ' &lsaquo; ', $pieces );

		/* translators: %1$s: updated title, %2$s: blog info name */
		return sprintf( __( '%1$s &lsaquo; %2$s', 'woocommerce' ), $title, get_bloginfo( 'name' ) );
	}

	/**
	 * Set up a div for the app to render into.
	 */
	public static function page_wrapper() {
		?>
		<div class="wrap">
			<div id="root"></div>
		</div>
		<?php
	}

	/**
	 * Hooks extra neccessary data into the component settings array already set in WooCommerce core.
	 *
	 * @param array $settings Array of component settings.
	 * @return array Array of component settings.
	 */
	public static function add_component_settings( $settings ) {
		if ( ! is_admin() ) {
			return $settings;
		}

		if ( ! function_exists( 'wc_blocks_container' ) ) {
			global $wp_locale;
			// inject data not available via older versions of wc_blocks/woo.
			$settings['orderStatuses'] = self::get_order_statuses( wc_get_order_statuses() );
			$settings['stockStatuses'] = self::get_order_statuses( wc_get_product_stock_status_options() );
			$settings['currency']      = self::get_currency_settings();
			$settings['locale']        = [
				'siteLocale'    => isset( $settings['siteLocale'] )
					? $settings['siteLocale']
					: get_locale(),
				'userLocale'    => isset( $settings['l10n']['userLocale'] )
					? $settings['l10n']['userLocale']
					: get_user_locale(),
				'weekdaysShort' => isset( $settings['l10n']['weekdaysShort'] )
					? $settings['l10n']['weekdaysShort']
					: array_values( $wp_locale->weekday_abbrev ),
			];
		}

		$preload_data_endpoints = apply_filters( 'woocommerce_component_settings_preload_endpoints', array() );
		if ( class_exists( 'Jetpack' ) ) {
			$preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection';
		}
		if ( ! empty( $preload_data_endpoints ) ) {
			$preload_data = array_reduce(
				array_values( $preload_data_endpoints ),
				'rest_preload_api_request'
			);
		}

		$preload_options = apply_filters( 'woocommerce_admin_preload_options', array() );
		if ( ! empty( $preload_options ) ) {
			foreach ( $preload_options as $option ) {
				$settings['preloadOptions'][ $option ] = get_option( $option );
			}
		}

		$preload_settings = apply_filters( 'woocommerce_admin_preload_settings', array() );
		if ( ! empty( $preload_settings ) ) {
			$setting_options = new \WC_REST_Setting_Options_V2_Controller();
			foreach ( $preload_settings as $group ) {
				$group_settings   = $setting_options->get_group_settings( $group );
				$preload_settings = [];
				foreach ( $group_settings as $option ) {
					if ( array_key_exists( 'id', $option ) && array_key_exists( 'value', $option ) ) {
						$preload_settings[ $option['id'] ] = $option['value'];
					}
				}
				$settings['preloadSettings'][ $group ] = $preload_settings;
			}
		}

		$user_controller = new \WP_REST_Users_Controller();
		$request         = new \WP_REST_Request();
		$request->set_query_params( array( 'context' => 'edit' ) );
		$user_response     = $user_controller->get_current_item( $request );
		$current_user_data = is_wp_error( $user_response ) ? (object) array() : $user_response->get_data();

		$settings['currentUserData']      = $current_user_data;
		$settings['reviewsEnabled']       = get_option( 'woocommerce_enable_reviews' );
		$settings['manageStock']          = get_option( 'woocommerce_manage_stock' );
		$settings['commentModeration']    = get_option( 'comment_moderation' );
		$settings['notifyLowStockAmount'] = get_option( 'woocommerce_notify_low_stock_amount' );
		// @todo On merge, once plugin images are added to core WooCommerce, `wcAdminAssetUrl` can be retired,
		// and `wcAssetUrl` can be used in its place throughout the codebase.
		$settings['wcAdminAssetUrl'] = WC_ADMIN_IMAGES_FOLDER_URL;
		$settings['wcVersion']       = WC_VERSION;
		$settings['siteUrl']         = site_url();
		$settings['shopUrl']         = get_permalink( wc_get_page_id( 'shop' ) );
		$settings['homeUrl']         = home_url();
		$settings['dateFormat']      = get_option( 'date_format' );
		$settings['timeZone']        = wc_timezone_string();
		$settings['plugins']         = array(
			'installedPlugins' => PluginsHelper::get_installed_plugin_slugs(),
			'activePlugins'    => Plugins::get_active_plugins(),
		);
		// Plugins that depend on changing the translation work on the server but not the client -
		// WooCommerce Branding is an example of this - so pass through the translation of
		// 'WooCommerce' to wcSettings.
		$settings['woocommerceTranslation'] = __( 'WooCommerce', 'woocommerce' );
		// We may have synced orders with a now-unregistered status.
		// E.g An extension that added statuses is now inactive or removed.
		$settings['unregisteredOrderStatuses'] = self::get_unregistered_order_statuses();
		// The separator used for attributes found in Variation titles.
		$settings['variationTitleAttributesSeparator'] = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', new \WC_Product() );

		if ( ! empty( $preload_data_endpoints ) ) {
			$settings['dataEndpoints'] = isset( $settings['dataEndpoints'] )
				? $settings['dataEndpoints']
				: [];
			foreach ( $preload_data_endpoints as $key => $endpoint ) {
				// Handle error case: rest_do_request() doesn't guarantee success.
				if ( empty( $preload_data[ $endpoint ] ) ) {
					$settings['dataEndpoints'][ $key ] = array();
				} else {
					$settings['dataEndpoints'][ $key ] = $preload_data[ $endpoint ]['body'];
				}
			}
		}
		$settings = self::get_custom_settings( $settings );
		if ( PageController::is_embed_page() ) {
			$settings['embedBreadcrumbs'] = self::get_embed_breadcrumbs();
		}

		$settings['allowMarketplaceSuggestions']      = WC_Marketplace_Suggestions::allow_suggestions();
		$settings['connectNonce']                     = wp_create_nonce( 'connect' );
		$settings['wcpay_welcome_page_connect_nonce'] = wp_create_nonce( 'wcpay-connect' );

		return $settings;
	}

	/**
	 * Format order statuses by removing a leading 'wc-' if present.
	 *
	 * @param array $statuses Order statuses.
	 * @return array formatted statuses.
	 */
	public static function get_order_statuses( $statuses ) {
		$formatted_statuses = array();
		foreach ( $statuses as $key => $value ) {
			$formatted_key                        = preg_replace( '/^wc-/', '', $key );
			$formatted_statuses[ $formatted_key ] = $value;
		}
		return $formatted_statuses;
	}

	/**
	 * Get all order statuses present in analytics tables that aren't registered.
	 *
	 * @return array Unregistered order statuses.
	 */
	public static function get_unregistered_order_statuses() {
		$registered_statuses   = wc_get_order_statuses();
		$all_synced_statuses   = OrdersDataStore::get_all_statuses();
		$unregistered_statuses = array_diff( $all_synced_statuses, array_keys( $registered_statuses ) );
		$formatted_status_keys = self::get_order_statuses( array_fill_keys( $unregistered_statuses, '' ) );
		$formatted_statuses    = array_keys( $formatted_status_keys );

		return array_combine( $formatted_statuses, $formatted_statuses );
	}

	/**
	 * Register the admin settings for use in the WC REST API
	 *
	 * @param array $groups Array of setting groups.
	 * @return array
	 */
	public static function add_settings_group( $groups ) {
		$groups[] = array(
			'id'          => 'wc_admin',
			'label'       => __( 'WooCommerce Admin', 'woocommerce' ),
			'description' => __( 'Settings for WooCommerce admin reporting.', 'woocommerce' ),
		);
		return $groups;
	}

	/**
	 * Add WC Admin specific settings
	 *
	 * @param array $settings Array of settings in wc admin group.
	 * @return array
	 */
	public static function add_settings( $settings ) {
		$unregistered_statuses = self::get_unregistered_order_statuses();
		$registered_statuses   = self::get_order_statuses( wc_get_order_statuses() );
		$all_statuses          = array_merge( $unregistered_statuses, $registered_statuses );

		$settings[] = array(
			'id'          => 'woocommerce_excluded_report_order_statuses',
			'option_key'  => 'woocommerce_excluded_report_order_statuses',
			'label'       => __( 'Excluded report order statuses', 'woocommerce' ),
			'description' => __( 'Statuses that should not be included when calculating report totals.', 'woocommerce' ),
			'default'     => array( 'pending', 'cancelled', 'failed' ),
			'type'        => 'multiselect',
			'options'     => $all_statuses,
		);
		$settings[] = array(
			'id'          => 'woocommerce_actionable_order_statuses',
			'option_key'  => 'woocommerce_actionable_order_statuses',
			'label'       => __( 'Actionable order statuses', 'woocommerce' ),
			'description' => __( 'Statuses that require extra action on behalf of the store admin.', 'woocommerce' ),
			'default'     => array( 'processing', 'on-hold' ),
			'type'        => 'multiselect',
			'options'     => $all_statuses,
		);
		$settings[] = array(
			'id'          => 'woocommerce_default_date_range',
			'option_key'  => 'woocommerce_default_date_range',
			'label'       => __( 'Default Date Range', 'woocommerce' ),
			'description' => __( 'Default Date Range', 'woocommerce' ),
			'default'     => 'period=month&compare=previous_year',
			'type'        => 'text',
		);
		return $settings;
	}

	/**
	 * Gets custom settings used for WC Admin.
	 *
	 * @param array $settings Array of settings to merge into.
	 * @return array
	 */
	public static function get_custom_settings( $settings ) {
		$wc_rest_settings_options_controller = new \WC_REST_Setting_Options_Controller();
		$wc_admin_group_settings             = $wc_rest_settings_options_controller->get_group_settings( 'wc_admin' );
		$settings['wcAdminSettings']         = array();

		foreach ( $wc_admin_group_settings as $setting ) {
			if ( ! empty( $setting['id'] ) ) {
				$settings['wcAdminSettings'][ $setting['id'] ] = $setting['value'];
			}
		}
		return $settings;
	}

	/**
	 * Return an object defining the currecy options for the site's current currency
	 *
	 * @return  array  Settings for the current currency {
	 *     Array of settings.
	 *
	 *     @type string $code       Currency code.
	 *     @type string $precision  Number of decimals.
	 *     @type string $symbol     Symbol for currency.
	 * }
	 */
	public static function get_currency_settings() {
		$code = get_woocommerce_currency();

		return apply_filters(
			'wc_currency_settings',
			array(
				'code'              => $code,
				'precision'         => wc_get_price_decimals(),
				'symbol'            => html_entity_decode( get_woocommerce_currency_symbol( $code ) ),
				'symbolPosition'    => get_option( 'woocommerce_currency_pos' ),
				'decimalSeparator'  => wc_get_price_decimal_separator(),
				'thousandSeparator' => wc_get_price_thousand_separator(),
				'priceFormat'       => html_entity_decode( get_woocommerce_price_format() ),
			)
		);
	}

	/**
	 * Delete woocommerce_onboarding_homepage_post_id field when the homepage is deleted
	 *
	 * @param int $post_id The deleted post id.
	 */
	public static function delete_homepage( $post_id ) {
		if ( 'page' !== get_post_type( $post_id ) ) {
			return;
		}
		$homepage_id = intval( get_option( 'woocommerce_onboarding_homepage_post_id', false ) );
		if ( $homepage_id === $post_id ) {
			delete_option( 'woocommerce_onboarding_homepage_post_id' );
		}
	}
}
Internal/Admin/RemoteFreeExtensions/EvaluateExtension.php000064400000002262152333011040017633 0ustar00<?php
/**
 * Evaluates the spec and returns a status.
 */

namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\PluginsHelper;
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RuleEvaluator;

/**
 * Evaluates the extension and returns it.
 */
class EvaluateExtension {
	/**
	 * Evaluates the extension and returns it.
	 *
	 * @param object $extension The extension to evaluate.
	 * @return object The evaluated extension.
	 */
	public static function evaluate( $extension ) {
		$rule_evaluator = new RuleEvaluator();

		if ( isset( $extension->is_visible ) ) {
			$is_visible            = $rule_evaluator->evaluate( $extension->is_visible );
			$extension->is_visible = $is_visible;
		} else {
			$extension->is_visible = true;
		}

		$installed_plugins       = PluginsHelper::get_installed_plugin_slugs();
		$activated_plugins       = PluginsHelper::get_active_plugin_slugs();
		$extension->is_installed = in_array( explode( ':', $extension->key )[0], $installed_plugins, true );
		$extension->is_activated = in_array( explode( ':', $extension->key )[0], $activated_plugins, true );

		return $extension;
	}
}
Internal/Admin/RemoteFreeExtensions/RemoteFreeExtensionsDataSourcePoller.php000064400000001371152333011040023436 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions;

/**
 * Specs data source poller class for remote free extensions.
 */
class RemoteFreeExtensionsDataSourcePoller extends \Automattic\WooCommerce\Admin\DataSourcePoller {

	const ID = 'remote_free_extensions';

	const DATA_SOURCES = array(
		'https://woocommerce.com/wp-json/wccom/obw-free-extensions/3.0/extensions.json',
	);

	/**
	 * Class instance.
	 *
	 * @var Analytics instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self(
				self::ID,
				self::DATA_SOURCES,
				array(
					'spec_key' => 'key',
				)
			);
		}
		return self::$instance;
	}
}
Internal/Admin/RemoteFreeExtensions/Init.php000064400000004004152333011040015067 0ustar00<?php
/**
 * Handles running payment method specs
 */

namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\DefaultFreeExtensions;

/**
 * Remote Payment Methods engine.
 * This goes through the specs and gets eligible payment methods.
 */
class Init {

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'woocommerce_updated', array( __CLASS__, 'delete_specs_transient' ) );
	}

	/**
	 * Go through the specs and run them.
	 *
	 * @param array $allowed_bundles Optional array of allowed bundles to be returned.
	 * @return array
	 */
	public static function get_extensions( $allowed_bundles = array() ) {
		$bundles = array();
		$specs   = self::get_specs();

		foreach ( $specs as $spec ) {
			$spec              = (object) $spec;
			$bundle            = (array) $spec;
			$bundle['plugins'] = array();

			if ( ! empty( $allowed_bundles ) && ! in_array( $spec->key, $allowed_bundles, true ) ) {
				continue;
			}

			foreach ( $spec->plugins as $plugin ) {
				$extension = EvaluateExtension::evaluate( (object) $plugin );

				if ( ! property_exists( $extension, 'is_visible' ) || $extension->is_visible ) {
					$bundle['plugins'][] = $extension;
				}
			}

			$bundles[] = $bundle;
		}

		return $bundles;
	}

	/**
	 * Delete the specs transient.
	 */
	public static function delete_specs_transient() {
		RemoteFreeExtensionsDataSourcePoller::get_instance()->delete_specs_transient();
	}

	/**
	 * Get specs or fetch remotely if they don't exist.
	 */
	public static function get_specs() {
		if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
			return DefaultFreeExtensions::get_all();
		}
		$specs = RemoteFreeExtensionsDataSourcePoller::get_instance()->get_specs_from_data_sources();

		// Fetch specs if they don't yet exist.
		if ( false === $specs || ! is_array( $specs ) || 0 === count( $specs ) ) {
			return DefaultFreeExtensions::get_all();
		}

		return $specs;
	}
}
Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php000064400000050440152333011040020437 0ustar00<?php
/**
 * Gets a list of fallback methods if remote fetching is disabled.
 */

namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions;

use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\DefaultPaymentGateways;

defined( 'ABSPATH' ) || exit;


/**
 * Default Free Extensions
 */
class DefaultFreeExtensions {

	/**
	 * Get default specs.
	 *
	 * @return array Default specs.
	 */
	public static function get_all() {
		$bundles = [
			[
				'key'     => 'obw/basics',
				'title'   => __( 'Get the basics', 'woocommerce' ),
				'plugins' => [
					self::get_plugin( 'woocommerce-payments' ),
					self::get_plugin( 'woocommerce-services:shipping' ),
					self::get_plugin( 'woocommerce-services:tax' ),
					self::get_plugin( 'jetpack' ),
				],
			],
			[
				'key'     => 'obw/grow',
				'title'   => __( 'Grow your store', 'woocommerce' ),
				'plugins' => [
					self::get_plugin( 'mailpoet' ),
					self::get_plugin( 'google-listings-and-ads' ),
					self::get_plugin( 'facebook-for-woocommerce' ),
					self::get_plugin( 'tiktok-for-business:alt' ),
				],
			],
			[
				'key'     => 'task-list/reach',
				'title'   => __( 'Reach out to customers', 'woocommerce' ),
				'plugins' => [
					self::get_plugin( 'mailpoet:alt' ),
					self::get_plugin( 'mailchimp-for-woocommerce' ),
					self::get_plugin( 'creative-mail-by-constant-contact' ),
				],
			],
			[
				'key'     => 'task-list/grow',
				'title'   => __( 'Grow your store', 'woocommerce' ),
				'plugins' => [
					self::get_plugin( 'google-listings-and-ads:alt' ),
					self::get_plugin( 'tiktok-for-business' ),
					self::get_plugin( 'facebook-for-woocommerce:alt' ),
					self::get_plugin( 'pinterest-for-woocommerce' ),
				],
			],
		];

		$bundles = wp_json_encode( $bundles );
		return json_decode( $bundles );
	}

	/**
	 * Get the plugin arguments by slug.
	 *
	 * @param string $slug Slug.
	 * @return array
	 */
	public static function get_plugin( $slug ) {
		$plugins = array(
			'google-listings-and-ads'           => [
				'name'           => __( 'Google Listings & Ads', 'woocommerce' ),
				'description'    => sprintf(
					/* translators: 1: opening product link tag. 2: closing link tag */
					__( 'Drive sales with %1$sGoogle Listings and Ads%2$s', 'woocommerce' ),
					'<a href="https://woocommerce.com/products/google-listings-and-ads" target="_blank">',
					'</a>'
				),
				'image_url'      => plugins_url( '/assets/images/onboarding/google-listings-and-ads.png', WC_PLUGIN_FILE ),
				'manage_url'     => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart',
				'is_built_by_wc' => true,
				'is_visible'     => [
					[
						'type'    => 'not',
						'operand' => [
							[
								'type'    => 'plugins_activated',
								'plugins' => [ 'google-listings-and-ads' ],
							],
						],
					],
				],
			],
			'google-listings-and-ads:alt'       => [
				'name'           => __( 'Google Listings & Ads', 'woocommerce' ),
				'description'    => __( 'Reach more shoppers and drive sales for your store. Integrate with Google to list your products for free and launch paid ad campaigns.', 'woocommerce' ),
				'image_url'      => plugins_url( '/assets/images/onboarding/google-listings-and-ads.png', WC_PLUGIN_FILE ),
				'manage_url'     => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart',
				'is_built_by_wc' => true,
			],
			'facebook-for-woocommerce'          => [
				'name'           => __( 'Facebook for WooCommerce', 'woocommerce' ),
				'description'    => __( 'List products and create ads on Facebook and Instagram with <a href="https://woocommerce.com/products/facebook/">Facebook for WooCommerce</a>', 'woocommerce' ),
				'image_url'      => plugins_url( '/assets/images/onboarding/facebook.png', WC_PLUGIN_FILE ),
				'manage_url'     => 'admin.php?page=wc-facebook',
				'is_built_by_wc' => false,
			],
			'facebook-for-woocommerce:alt'      => [
				'name'           => __( 'Facebook for WooCommerce', 'woocommerce' ),
				'description'    => __( 'List products and create ads on Facebook and Instagram.', 'woocommerce' ),
				'image_url'      => plugins_url( '/assets/images/onboarding/facebook.png', WC_PLUGIN_FILE ),
				'manage_url'     => 'admin.php?page=wc-facebook',
				'is_built_by_wc' => false,
			],
			'pinterest-for-woocommerce'         => [
				'name'           => __( 'Pinterest for WooCommerce', 'woocommerce' ),
				'description'    => __( 'Get your products in front of Pinterest users searching for ideas and things to buy. Get started with Pinterest and make your entire product catalog browsable.', 'woocommerce' ),
				'image_url'      => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ),
				'manage_url'     => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding',
				'is_built_by_wc' => true,
			],
			'mailpoet'                          => [
				'name'           => __( 'MailPoet', 'woocommerce' ),
				'description'    => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ),
				'image_url'      => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ),
				'manage_url'     => 'admin.php?page=mailpoet-newsletters',
				'is_built_by_wc' => true,
			],
			'mailchimp-for-woocommerce'         => [
				'name'           => __( 'Mailchimp', 'woocommerce' ),
				'description'    => __( 'Send targeted campaigns, recover abandoned carts and much more with Mailchimp.', 'woocommerce' ),
				'image_url'      => plugins_url( '/assets/images/onboarding/mailchimp-for-woocommerce.png', WC_PLUGIN_FILE ),
				'manage_url'     => 'admin.php?page=mailchimp-woocommerce',
				'is_built_by_wc' => false,
			],
			'creative-mail-by-constant-contact' => [
				'name'           => __( 'Creative Mail for WooCommerce', 'woocommerce' ),
				'description'    => __( 'Create on-brand store campaigns, fast email promotions and customer retargeting with Creative Mail.', 'woocommerce' ),
				'image_url'      => plugins_url( '/assets/images/onboarding/creative-mail-by-constant-contact.png', WC_PLUGIN_FILE ),
				'manage_url'     => 'admin.php?page=creativemail',
				'is_built_by_wc' => false,
			],
			'woocommerce-payments'              => [
				'description'    => sprintf(
					/* translators: 1: opening product link tag. 2: closing link tag */
					__( 'Accept credit cards and other popular payment methods with %1$sWooCommerce Payments%2$s', 'woocommerce' ),
					'<a href="https://woocommerce.com/products/woocommerce-payments" target="_blank">',
					'</a>'
				),
				'is_visible'     => [
					[
						'type'     => 'or',
						'operands' => [
							[
								'type'      => 'base_location_country',
								'value'     => 'US',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'PR',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'AU',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'CA',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'DE',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'ES',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'FR',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'GB',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'IE',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'IT',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'NZ',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'AT',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'BE',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'NL',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'PL',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'PT',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'CH',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'HK',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'SG',
								'operation' => '=',
							],
						],
					],
					DefaultPaymentGateways::get_rules_for_cbd( false ),
				],
				'is_built_by_wc' => true,
			],
			'woocommerce-services:shipping'     => [
				'description'    => sprintf(
				/* translators: 1: opening product link tag. 2: closing link tag */
					__( 'Print shipping labels with %1$sWooCommerce Shipping%2$s', 'woocommerce' ),
					'<a href="https://woocommerce.com/products/shipping" target="_blank">',
					'</a>'
				),
				'is_visible'     => [
					[
						'type'      => 'base_location_country',
						'value'     => 'US',
						'operation' => '=',
					],
					[
						'type'    => 'not',
						'operand' => [
							[
								'type'    => 'plugins_activated',
								'plugins' => [ 'woocommerce-services' ],
							],
						],
					],
					[
						'type'     => 'or',
						'operands' => [
							[
								[
									'type'         => 'option',
									'transformers' => [
										[
											'use'       => 'dot_notation',
											'arguments' => [
												'path' => 'product_types',
											],
										],
										[
											'use' => 'count',
										],
									],
									'option_name'  => 'woocommerce_onboarding_profile',
									'value'        => 1,
									'default'      => array(),
									'operation'    => '!=',
								],
							],
							[
								[
									'type'         => 'option',
									'transformers' => [
										[
											'use'       => 'dot_notation',
											'arguments' => [
												'path' => 'product_types.0',
											],
										],
									],
									'option_name'  => 'woocommerce_onboarding_profile',
									'value'        => 'downloads',
									'default'      => '',
									'operation'    => '!=',
								],
							],
						],
					],
				],
				'is_built_by_wc' => true,
			],
			'woocommerce-services:tax'          => [
				'description'    => sprintf(
					/* translators: 1: opening product link tag. 2: closing link tag */
					__( 'Get automated sales tax with %1$sWooCommerce Tax%2$s', 'woocommerce' ),
					'<a href="https://woocommerce.com/products/tax" target="_blank">',
					'</a>'
				),
				'is_visible'     => [
					[
						'type'     => 'or',
						'operands' => [
							[
								'type'      => 'base_location_country',
								'value'     => 'US',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'FR',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'GB',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'DE',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'CA',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'AU',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'GR',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'BE',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'PT',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'DK',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'SE',
								'operation' => '=',
							],
						],
					],
					[
						'type'    => 'not',
						'operand' => [
							[
								'type'    => 'plugins_activated',
								'plugins' => [ 'woocommerce-services' ],
							],
						],
					],
				],
				'is_built_by_wc' => true,
			],
			'jetpack'                           => [
				'description'    => sprintf(
					/* translators: 1: opening product link tag. 2: closing link tag */
					__( 'Enhance speed and security with %1$sJetpack%2$s', 'woocommerce' ),
					'<a href="https://woocommerce.com/products/jetpack" target="_blank">',
					'</a>'
				),
				'is_visible'     => [
					[
						'type'    => 'not',
						'operand' => [
							[
								'type'    => 'plugins_activated',
								'plugins' => [ 'jetpack' ],
							],
						],
					],
				],
				'is_built_by_wc' => false,
			],
			'mailpoet'                          => [
				'name'           => __( 'MailPoet', 'woocommerce' ),
				'description'    => sprintf(
					/* translators: 1: opening product link tag. 2: closing link tag */
					__( 'Level up your email marketing with %1$sMailPoet%2$s', 'woocommerce' ),
					'<a href="https://woocommerce.com/products/mailpoet" target="_blank">',
					'</a>'
				),
				'manage_url'     => 'admin.php?page=mailpoet-newsletters',
				'is_visible'     => [
					[
						'type'    => 'not',
						'operand' => [
							[
								'type'    => 'plugins_activated',
								'plugins' => [ 'mailpoet' ],
							],
						],
					],
				],
				'is_built_by_wc' => true,
			],
			'mailpoet:alt'                      => [
				'name'           => __( 'MailPoet', 'woocommerce' ),
				'description'    => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ),
				'image_url'      => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ),
				'manage_url'     => 'admin.php?page=mailpoet-newsletters',
				'is_built_by_wc' => true,
			],
			'tiktok-for-business'               => [
				'name'           => __( 'TikTok for WooCommerce', 'woocommerce' ),
				'image_url'      => plugins_url( '/assets/images/onboarding/tiktok.svg', WC_PLUGIN_FILE ),
				'description'    =>
					__( 'Grow your online sales by promoting your products on TikTok to over one billion monthly active users around the world.', 'woocommerce' ),
				'manage_url'     => 'admin.php?page=tiktok',
				'is_visible'     => [
					[
						'type'     => 'or',
						'operands' => [
							[
								'type'      => 'base_location_country',
								'value'     => 'US',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'CA',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'MX',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'AT',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'BE',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'CZ',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'DK',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'FI',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'FR',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'DE',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'GR',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'HU',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'IE',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'IT',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'NL',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'PL',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'PT',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'RO',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'ES',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'SE',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'GB',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'CH',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'NO',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'AU',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'NZ',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'SG',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'MY',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'PH',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'ID',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'VN',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'TH',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'KR',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'IL',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'AE',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'RU',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'UA',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'TR',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'SA',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'BR',
								'operation' => '=',
							],
							[
								'type'      => 'base_location_country',
								'value'     => 'JP',
								'operation' => '=',
							],
						],
					],
				],
				'is_built_by_wc' => false,
			],
			'tiktok-for-business:alt'               => [
				'name'           => __( 'TikTok for WooCommerce', 'woocommerce' ),
				'image_url'      => plugins_url( '/assets/images/onboarding/tiktok.svg', WC_PLUGIN_FILE ),
				'description'    => sprintf(
					/* translators: 1: opening product link tag. 2: closing link tag */
					__( 'Create ad campaigns and reach one billion global users with %1$sTikTok for WooCommerce%2$s', 'woocommerce' ),
					'<a href="https://woocommerce.com/products/tiktok-for-woocommerce" target="_blank">',
					'</a>'
				),
				'manage_url'     => 'admin.php?page=tiktok',
				'is_built_by_wc' => false,
			],
		);

		$plugin        = $plugins[ $slug ];
		$plugin['key'] = $slug;

		return $plugin;
	}
}
Internal/Admin/ProductReviews/ReviewsCommentsOverrides.php000064400000010201152333011040020045 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Admin\ProductReviews;

use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use WP_Comment_Query;

/**
 * Tweaks the WordPress comments page to exclude reviews.
 */
class ReviewsCommentsOverrides {

	use AccessiblePrivateMethods;

	const REVIEWS_MOVED_NOTICE_ID = 'product_reviews_moved';

	/**
	 * Constructor.
	 */
	public function __construct() {
		self::add_action( 'admin_notices', array( $this, 'display_notices' ) );
		self::add_filter( 'woocommerce_dismiss_admin_notice_capability', array( $this, 'get_dismiss_capability' ), 10, 2 );
		self::add_filter( 'comments_list_table_query_args', array( $this, 'exclude_reviews_from_comments' ) );
	}

	/**
	 * Renders admin notices.
	 */
	protected function display_notices() : void {
		$screen = get_current_screen();

		if ( empty( $screen ) || $screen->base !== 'edit-comments' ) {
			return;
		}

		$this->maybe_display_reviews_moved_notice();
	}

	/**
	 * May render an admin notice informing the user that reviews were moved to a new page.
	 *
	 * @return void
	 */
	protected function maybe_display_reviews_moved_notice() : void {
		if ( $this->should_display_reviews_moved_notice() ) {
			$this->display_reviews_moved_notice();
		}
	}

	/**
	 * Checks if the admin notice informing the user that reviews were moved to a new page should be displayed.
	 *
	 * @return bool
	 */
	protected function should_display_reviews_moved_notice() : bool {
		// Do not display if the user does not have the capability  to see the new page.
		if ( ! WC()->call_function( 'current_user_can', Reviews::get_capability() ) ) {
			return false;
		}

		// Do not display if the current user has dismissed this notice.
		if ( WC()->call_function( 'get_user_meta', get_current_user_id(), 'dismissed_' . static::REVIEWS_MOVED_NOTICE_ID . '_notice', true ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Renders an admin notice informing the user that reviews were moved to a new page.
	 *
	 * @return void
	 */
	protected function display_reviews_moved_notice() : void {
		$dismiss_url = wp_nonce_url(
			add_query_arg(
				[
					'wc-hide-notice' => urlencode( static::REVIEWS_MOVED_NOTICE_ID ),
				]
			),
			'woocommerce_hide_notices_nonce',
			'_wc_notice_nonce'
		);
		?>

		<div class="notice notice-info is-dismissible">
			<p><strong><?php esc_html_e( 'Product reviews have moved!', 'woocommerce' ); ?></strong></p>
			<p><?php esc_html_e( 'Product reviews can now be managed from Products > Reviews.', 'woocommerce' ); ?></p>
			<p class="submit">
				<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=product&page=product-reviews' ) ); ?>" class="button-primary"><?php esc_html_e( 'Visit new location', 'woocommerce' ); ?></a>
			</p>
			<button type="button" class="notice-dismiss" onclick="window.location = '<?php echo esc_url( $dismiss_url ); ?>';"><span class="screen-reader-text"><?php esc_html_e( 'Dismiss this notice.', 'woocommerce' ); ?></span></button>
		</div>

		<?php
	}

	/**
	 * Gets the capability required to dismiss the notice.
	 *
	 * This is required so that users who do not have the manage_woocommerce capability (e.g. Editors) can still dismiss
	 * the notice displayed in the Comments page.
	 *
	 * @param string|mixed $default_capability The default required capability.
	 * @param string|mixed $notice_name The notice name.
	 * @return string
	 */
	protected function get_dismiss_capability( $default_capability, $notice_name ) {
		return $notice_name === self::REVIEWS_MOVED_NOTICE_ID ? Reviews::get_capability() : $default_capability;
	}

	/**
	 * Excludes product reviews from showing in the comments page.
	 *
	 * @param array|mixed $args {@see WP_Comment_Query} query args.
	 * @return array
	 */
	protected function exclude_reviews_from_comments( $args ) : array {

		if ( ! empty( $args['post_type'] ) && $args['post_type'] !== 'any' ) {
			$post_types = (array) $args['post_type'];
		} else {
			$post_types = get_post_types();
		}

		$index = array_search( 'product', $post_types );

		if ( $index !== false ) {
			unset( $post_types[ $index ] );
		}

		if ( ! is_array( $args ) ) {
			$args = [];
		}

		$args['post_type'] = $post_types;

		return $args;
	}

}
Internal/Admin/ProductReviews/Reviews.php000064400000052100152333011040014460 0ustar00<?php
/**
 * Products > Reviews
 */

namespace Automattic\WooCommerce\Internal\Admin\ProductReviews;

use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use WP_Ajax_Response;
use WP_Comment;
use WP_Screen;

/**
 * Handles backend logic for the Reviews component.
 */
class Reviews {

	use AccessiblePrivateMethods;

	/**
	 * Admin page identifier.
	 */
	const MENU_SLUG = 'product-reviews';

	/**
	 * Reviews page hook name.
	 *
	 * @var string|null
	 */
	protected $reviews_page_hook = null;

	/**
	 * Reviews list table instance.
	 *
	 * @var ReviewsListTable|null
	 */
	protected $reviews_list_table;

	/**
	 * Constructor.
	 */
	public function __construct() {

		self::add_action( 'admin_menu', [ $this, 'add_reviews_page' ] );
		self::add_action( 'admin_enqueue_scripts', [ $this, 'load_javascript' ] );

		// These ajax callbacks need a low priority to ensure they run before their WordPress core counterparts.
		self::add_action( 'wp_ajax_edit-comment', [ $this, 'handle_edit_review' ], -1 );
		self::add_action( 'wp_ajax_replyto-comment', [ $this, 'handle_reply_to_review' ], -1 );

		self::add_filter( 'parent_file', [ $this, 'edit_review_parent_file' ] );
		self::add_filter( 'gettext', [ $this, 'edit_comments_screen_text' ], 10, 2 );
		self::add_action( 'admin_notices', [ $this, 'display_notices' ] );
	}

	/**
	 * Gets the required capability to access the reviews page and manage product reviews.
	 *
	 * @param string $context The context for which the capability is needed (e.g. `view` or `moderate`).
	 * @return string
	 */
	public static function get_capability( string $context = 'view' ) : string {

		/**
		 * Filters whether the current user can manage product reviews.
		 *
		 * This is aligned to {@see \wc_rest_check_product_reviews_permissions()}
		 *
		 * @since 6.7.0
		 *
		 * @param string $capability The capability (defaults to `moderate_comments` for viewing and `edit_products` for editing).
		 * @param string $context    The context for which the capability is needed.
		 */
		return (string) apply_filters( 'woocommerce_product_reviews_page_capability', $context === 'view' ? 'moderate_comments' : 'edit_products', $context );
	}

	/**
	 * Registers the Product Reviews submenu page.
	 *
	 * @return void
	 */
	private function add_reviews_page() : void {

		$this->reviews_page_hook = add_submenu_page(
			'edit.php?post_type=product',
			__( 'Reviews', 'woocommerce' ),
			__( 'Reviews', 'woocommerce' ) . $this->get_pending_count_bubble(),
			static::get_capability(),
			static::MENU_SLUG,
			[ $this, 'render_reviews_list_table' ]
		);

		self::add_action( "load-{$this->reviews_page_hook}", array( $this, 'load_reviews_screen' ) );
	}

	/**
	 * Retrieves the URL to the product reviews page.
	 *
	 * @return string
	 */
	public static function get_reviews_page_url() : string {
		return add_query_arg(
			[
				'post_type' => 'product',
				'page'      => static::MENU_SLUG,
			],
			admin_url( 'edit.php' )
		);
	}

	/**
	 * Determines whether the current page is the reviews page.
	 *
	 * @global WP_Screen $current_screen
	 *
	 * @return bool
	 */
	public function is_reviews_page() : bool {
		global $current_screen;

		return isset( $current_screen->base ) && $current_screen->base === 'product_page_' . static::MENU_SLUG;
	}

	/**
	 * Loads the JavaScript required for inline replies and quick edit.
	 *
	 * @return void
	 */
	private function load_javascript() : void {
		if ( $this->is_reviews_page() ) {
			wp_enqueue_script( 'admin-comments' );
			enqueue_comment_hotkeys_js();
		}
	}

	/**
	 * Determines if the object is a review or a reply to a review.
	 *
	 * @param WP_Comment|mixed $object Object to check.
	 * @return bool
	 */
	protected function is_review_or_reply( $object ) : bool {

		$is_review_or_reply = $object instanceof WP_Comment && in_array( $object->comment_type, [ 'review', 'comment' ], true ) && get_post_type( $object->comment_post_ID ) === 'product';

		/**
		 * Filters whether the object is a review or a reply to a review.
		 *
		 * @since 6.7.0
		 *
		 * @param bool             $is_review_or_reply Whether the object in context is a review or a reply to a review.
		 * @param WP_Comment|mixed $object             The object in context.
		 */
		return (bool) apply_filters( 'woocommerce_product_reviews_is_product_review_or_reply', $is_review_or_reply, $object );
	}

	/**
	 * Ajax callback for editing a review.
	 *
	 * This functionality is taken from {@see wp_ajax_edit_comment()} and is largely copy and pasted. The only thing
	 * we want to change is the review row HTML in the response. WordPress core uses a comment list table and we need
	 * to use our own {@see ReviewsListTable} class to support our custom columns.
	 *
	 * This ajax callback is registered with a lower priority than WordPress core's so that our code can run
	 * first. If the supplied comment ID is not a review or a reply to a review, then we `return` early from this method
	 * to allow the WordPress core callback to take over.
	 *
	 * @return void
	 */
	private function handle_edit_review(): void {
		check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' );

		$comment_id = isset( $_POST['comment_ID'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['comment_ID'] ) ) : 0;

		if ( empty( $comment_id ) || ! current_user_can( 'edit_comment', $comment_id ) ) {
			wp_die( -1 );
		}

		$review = get_comment( $comment_id );

		// Bail silently if this is not a review, or a reply to a review. That allows `wp_ajax_edit_comment()` to handle any further actions.
		if ( ! $this->is_review_or_reply( $review ) ) {
			return;
		}

		if ( empty( $review->comment_ID ) ) {
			wp_die( -1 );
		}

		if ( empty( $_POST['content'] ) ) {
			wp_die( esc_html__( 'Error: Please type your review text.', 'woocommerce' ) );
		}

		if ( isset( $_POST['status'] ) ) {
			$_POST['comment_status'] = sanitize_text_field( wp_unslash( $_POST['status'] ) );
		}

		$updated = edit_comment();
		if ( is_wp_error( $updated ) ) {
			wp_die( esc_html( $updated->get_error_message() ) );
		}

		$position      = isset( $_POST['position'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['position'] ) ) : -1;
		$wp_list_table = $this->make_reviews_list_table();

		ob_start();
		$wp_list_table->single_row( $review );
		$review_list_item = ob_get_clean();

		$x = new WP_Ajax_Response();

		$x->add(
			array(
				'what'     => 'edit_comment',
				'id'       => $review->comment_ID,
				'data'     => $review_list_item,
				'position' => $position,
			)
		);

		$x->send();
	}

	/**
	 * Ajax callback for replying to a review inline.
	 *
	 * This functionality is taken from {@see wp_ajax_replyto_comment()} and is largely copy and pasted. The only thing
	 * we want to change is the review row HTML in the response. WordPress core uses a comment list table and we need
	 * to use our own {@see ReviewsListTable} class to support our custom columns.
	 *
	 * This ajax callback is registered with a lower priority than WordPress core's so that our code can run
	 * first. If the supplied comment ID is not a review or a reply to a review, then we `return` early from this method
	 * to allow the WordPress core callback to take over.
	 *
	 * @return void
	 */
	private function handle_reply_to_review() : void {
		check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' );

		$comment_post_ID = isset( $_POST['comment_post_ID'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['comment_post_ID'] ) ) : 0; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
		$post            = get_post( $comment_post_ID ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase

		if ( ! $post ) {
			wp_die( -1 );
		}

		// Inline Review replies will use the `detail` mode. If that's not what we have, then let WordPress core take over.
		if ( isset( $_REQUEST['mode'] ) && $_REQUEST['mode'] === 'dashboard' ) {
			return;
		}

		// If this is not a a reply to a review, bail silently to let WordPress core take over.
		if ( get_post_type( $post ) !== 'product' ) {
			return;
		}

		if ( ! current_user_can( 'edit_post', $comment_post_ID ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
			wp_die( -1 );
		}

		if ( empty( $post->post_status ) ) {
			wp_die( 1 );
		} elseif ( in_array( $post->post_status, array( 'draft', 'pending', 'trash' ), true ) ) {
			wp_die( esc_html__( 'Error: You can\'t reply to a review on a draft product.', 'woocommerce' ) );
		}

		$user = wp_get_current_user();

		if ( $user->exists() ) {
			$user_ID              = $user->ID;
			$comment_author       = wp_slash( $user->display_name );
			$comment_author_email = wp_slash( $user->user_email );
			$comment_author_url   = wp_slash( $user->user_url );
			// WordPress core already sanitizes `content` during the `pre_comment_content` hook, which is why it's not needed here, {@see wp_filter_comment()} and {@see kses_init_filters()}.
			$comment_content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$comment_type    = isset( $_POST['comment_type'] ) ? sanitize_text_field( wp_unslash( $_POST['comment_type'] ) ) : 'comment';

			if ( current_user_can( 'unfiltered_html' ) ) {
				if ( ! isset( $_POST['_wp_unfiltered_html_comment'] ) ) {
					$_POST['_wp_unfiltered_html_comment'] = '';
				}

				if ( wp_create_nonce( 'unfiltered-html-comment' ) != $_POST['_wp_unfiltered_html_comment'] ) {
					kses_remove_filters(); // Start with a clean slate.
					kses_init_filters();   // Set up the filters.
					remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
					add_filter( 'pre_comment_content', 'wp_filter_kses' );
				}
			}
		} else {
			wp_die( esc_html__( 'Sorry, you must be logged in to reply to a review.', 'woocommerce' ) );
		}

		if ( $comment_content === '' ) {
			wp_die( esc_html__( 'Error: Please type your reply text.', 'woocommerce' ) );
		}

		$comment_parent = 0;

		if ( isset( $_POST['comment_ID'] ) ) {
			$comment_parent = absint( wp_unslash( $_POST['comment_ID'] ) );
		}

		$comment_auto_approved = false;
		$commentdata           = compact( 'comment_post_ID', 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_content', 'comment_type', 'comment_parent', 'user_ID' );

		// Automatically approve parent comment.
		if ( ! empty( $_POST['approve_parent'] ) ) {
			$parent = get_comment( $comment_parent );

			if ( $parent && $parent->comment_approved === '0' && $parent->comment_post_ID === $comment_post_ID ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
				if ( ! current_user_can( 'edit_comment', $parent->comment_ID ) ) {
					wp_die( -1 );
				}

				if ( wp_set_comment_status( $parent, 'approve' ) ) {
					$comment_auto_approved = true;
				}
			}
		}

		$comment_id = wp_new_comment( $commentdata );

		if ( is_wp_error( $comment_id ) ) {
			wp_die( esc_html( $comment_id->get_error_message() ) );
		}

		$comment = get_comment( $comment_id );

		if ( ! $comment ) {
			wp_die( 1 );
		}

		$position = ( isset( $_POST['position'] ) && (int) $_POST['position'] ) ? (int) $_POST['position'] : '-1';

		ob_start();
		$wp_list_table = $this->make_reviews_list_table();
		$wp_list_table->single_row( $comment );
		$comment_list_item = ob_get_clean();

		$response = array(
			'what'     => 'comment',
			'id'       => $comment->comment_ID,
			'data'     => $comment_list_item,
			'position' => $position,
		);

		$counts                   = wp_count_comments();
		$response['supplemental'] = array(
			'in_moderation'        => $counts->moderated,
			'i18n_comments_text'   => sprintf(
			/* translators: %s: Number of reviews. */
				_n( '%s Review', '%s Reviews', $counts->approved, 'woocommerce' ),
				number_format_i18n( $counts->approved )
			),
			'i18n_moderation_text' => sprintf(
			/* translators: %s: Number of reviews. */
				_n( '%s Review in moderation', '%s Reviews in moderation', $counts->moderated, 'woocommerce' ),
				number_format_i18n( $counts->moderated )
			),
		);

		if ( $comment_auto_approved && isset( $parent ) ) {
			$response['supplemental']['parent_approved'] = $parent->comment_ID;
			$response['supplemental']['parent_post_id']  = $parent->comment_post_ID;
		}

		$x = new WP_Ajax_Response();
		$x->add( $response );
		$x->send();
	}

	/**
	 * Displays notices on the Reviews page.
	 *
	 * @return void
	 */
	protected function display_notices() : void {

		if ( $this->is_reviews_page() ) {
			$this->maybe_display_reviews_bulk_action_notice();
		}
	}

	/**
	 * May display the bulk action admin notice.
	 *
	 * @return void
	 */
	protected function maybe_display_reviews_bulk_action_notice() : void {

		$messages = $this->get_bulk_action_notice_messages();

		echo ! empty( $messages ) ? '<div id="moderated" class="updated"><p>' . implode( "<br/>\n", $messages ) . '</p></div>' : '';  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Gets the applicable bulk action admin notice messages.
	 *
	 * @return array
	 */
	protected function get_bulk_action_notice_messages() : array {

		$approved   = isset( $_REQUEST['approved'] ) ? (int) $_REQUEST['approved'] : 0;
		$unapproved = isset( $_REQUEST['unapproved'] ) ? (int) $_REQUEST['unapproved'] : 0;
		$deleted    = isset( $_REQUEST['deleted'] ) ? (int) $_REQUEST['deleted'] : 0;
		$trashed    = isset( $_REQUEST['trashed'] ) ? (int) $_REQUEST['trashed'] : 0;
		$untrashed  = isset( $_REQUEST['untrashed'] ) ? (int) $_REQUEST['untrashed'] : 0;
		$spammed    = isset( $_REQUEST['spammed'] ) ? (int) $_REQUEST['spammed'] : 0;
		$unspammed  = isset( $_REQUEST['unspammed'] ) ? (int) $_REQUEST['unspammed'] : 0;

		$messages = [];

		if ( $approved > 0 ) {
			/* translators: %s is an integer higher than 0 (1, 2, 3...) */
			$messages[] = sprintf( _n( '%s review approved', '%s reviews approved', $approved, 'woocommerce' ), $approved );
		}

		if ( $unapproved > 0 ) {
			/* translators: %s is an integer higher than 0 (1, 2, 3...) */
			$messages[] = sprintf( _n( '%s review unapproved', '%s reviews unapproved', $unapproved, 'woocommerce' ), $unapproved );
		}

		if ( $spammed > 0 ) {
			$ids = isset( $_REQUEST['ids'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ids'] ) ) : 0;
			/* translators: %s is an integer higher than 0 (1, 2, 3...) */
			$messages[] = sprintf( _n( '%s review marked as spam.', '%s reviews marked as spam.', $spammed, 'woocommerce' ), $spammed ) . ' <a href="' . esc_url( wp_nonce_url( "edit-comments.php?doaction=undo&action=unspam&ids=$ids", 'bulk-comments' ) ) . '">' . __( 'Undo', 'woocommerce' ) . '</a><br />';
		}

		if ( $unspammed > 0 ) {
			/* translators: %s is an integer higher than 0 (1, 2, 3...) */
			$messages[] = sprintf( _n( '%s review restored from the spam', '%s reviews restored from the spam', $unspammed, 'woocommerce' ), $unspammed );
		}

		if ( $trashed > 0 ) {
			$ids = isset( $_REQUEST['ids'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ids'] ) ) : 0;
			/* translators: %s is an integer higher than 0 (1, 2, 3...) */
			$messages[] = sprintf( _n( '%s review moved to the Trash.', '%s reviews moved to the Trash.', $trashed, 'woocommerce' ), $trashed ) . ' <a href="' . esc_url( wp_nonce_url( "edit-comments.php?doaction=undo&action=untrash&ids=$ids", 'bulk-comments' ) ) . '">' . __( 'Undo', 'woocommerce' ) . '</a><br />';
		}

		if ( $untrashed > 0 ) {
			/* translators: %s is an integer higher than 0 (1, 2, 3...) */
			$messages[] = sprintf( _n( '%s review restored from the Trash', '%s reviews restored from the Trash', $untrashed, 'woocommerce' ), $untrashed );
		}

		if ( $deleted > 0 ) {
			/* translators: %s is an integer higher than 0 (1, 2, 3...) */
			$messages[] = sprintf( _n( '%s review permanently deleted', '%s reviews permanently deleted', $deleted, 'woocommerce' ), $deleted );
		}

		return $messages;
	}

	/**
	 * Counts the number of pending product reviews/replies, and returns the notification bubble if there's more than zero.
	 *
	 * @return string Empty string if there are no pending reviews, or bubble HTML if there are.
	 */
	protected function get_pending_count_bubble() : string {
		$count = (int) get_comments(
			[
				'type__in'  => [ 'review', 'comment' ],
				'status'    => '0',
				'post_type' => 'product',
				'count'     => true,
			]
		);

		/**
		 * Provides an opportunity to alter the pending comment count used within
		 * the product reviews admin list table.
		 *
		 * @since 7.0.0
		 *
		 * @param array $count Current count of comments pending review.
		 */
		$count = apply_filters( 'woocommerce_product_reviews_pending_count', $count );

		if ( empty( $count ) ) {
			return '';
		}

		return ' <span class="awaiting-mod count-' . esc_attr( $count ) . '"><span class="pending-count">' . esc_html( $count ) . '</span></span>';
	}

	/**
	 * Highlights Product -> Reviews admin menu item when editing a review or a reply to a review.
	 *
	 * @global string $submenu_file
	 *
	 * @param string|mixed $parent_file Parent menu item.
	 * @return string
	 */
	protected function edit_review_parent_file( $parent_file ) {
		global $submenu_file, $current_screen;

		if ( isset( $current_screen->id, $_GET['c'] ) && $current_screen->id === 'comment' ) {

			$comment_id = absint( $_GET['c'] );
			$comment    = get_comment( $comment_id );

			if ( isset( $comment->comment_parent ) && $comment->comment_parent > 0 ) {
				$comment = get_comment( $comment->comment_parent );
			}

			if ( isset( $comment->comment_post_ID ) && get_post_type( $comment->comment_post_ID ) === 'product' ) {
				$parent_file  = 'edit.php?post_type=product';
				$submenu_file = 'product-reviews'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
			}
		}

		return $parent_file;
	}

	/**
	 * Replaces Edit/Moderate Comment title/headline with Edit Review, when editing/moderating a review.
	 *
	 * @param  string|mixed $translation Translated text.
	 * @param  string|mixed $text        Text to translate.
	 * @return string|mixed              Translated text.
	 */
	protected function edit_comments_screen_text( $translation, $text ) {
		global $comment;

		// Bail out if not a text we should replace.
		if ( ! in_array( $text, [ 'Edit Comment', 'Moderate Comment' ], true ) ) {
			return $translation;
		}

		// Try to get comment from query params when not in context already.
		if ( ! $comment && isset( $_GET['action'], $_GET['c'] ) && $_GET['action'] === 'editcomment' ) {
			$comment_id = absint( $_GET['c'] );
			$comment    = get_comment( $comment_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
		}

		$is_reply = false;

		if ( isset( $comment->comment_parent ) && $comment->comment_parent > 0 ) {
			$is_reply = true;
			$comment  = get_comment( $comment->comment_parent ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
		}

		// Only replace the translated text if we are editing a comment left on a product (ie. a review).
		if ( isset( $comment->comment_post_ID ) && get_post_type( $comment->comment_post_ID ) === 'product' ) {
			if ( $text === 'Edit Comment' ) {
				$translation = $is_reply
					? __( 'Edit Review Reply', 'woocommerce' )
					: __( 'Edit Review', 'woocommerce' );
			} elseif ( $text === 'Moderate Comment' ) {
				$translation = $is_reply
					? __( 'Moderate Review Reply', 'woocommerce' )
					: __( 'Moderate Review', 'woocommerce' );
			}
		}

		return $translation;
	}

	/**
	 * Returns a new instance of `ReviewsListTable`, with the screen argument specified.
	 *
	 * @return ReviewsListTable
	 */
	protected function make_reviews_list_table() : ReviewsListTable {
		return new ReviewsListTable( [ 'screen' => $this->reviews_page_hook ? $this->reviews_page_hook : 'product_page_product-reviews' ] );
	}

	/**
	 * Initializes the list table.
	 *
	 * @return void
	 */
	protected function load_reviews_screen() : void {
		$this->reviews_list_table = $this->make_reviews_list_table();
		$this->reviews_list_table->process_bulk_action();
	}

	/**
	 * Renders the Reviews page.
	 *
	 * @return void
	 */
	public function render_reviews_list_table() : void {

		$this->reviews_list_table->prepare_items();

		ob_start();

		?>
		<div class="wrap">
			<h2><?php echo esc_html( get_admin_page_title() ); ?></h2>

			<?php $this->reviews_list_table->views(); ?>

			<form id="reviews-filter" method="get">
				<?php $page = isset( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : static::MENU_SLUG; ?>

				<input type="hidden" name="page" value="<?php echo esc_attr( $page ); ?>" />
				<input type="hidden" name="post_type" value="product" />
				<input type="hidden" name="pagegen_timestamp" value="<?php echo esc_attr( current_time( 'mysql', true ) ); ?>" />

				<?php $this->reviews_list_table->search_box( __( 'Search Reviews', 'woocommerce' ), 'reviews' ); ?>

				<?php $this->reviews_list_table->display(); ?>
			</form>
		</div>
		<?php
		wp_comment_reply( '-1', true, 'detail' );
		wp_comment_trashnotice();

		/**
		 * Filters the contents of the product reviews list table output.
		 *
		 * @since 6.7.0
		 *
		 * @param string           $output             The HTML output of the list table.
		 * @param ReviewsListTable $reviews_list_table The reviews list table instance.
		 */
		echo apply_filters( 'woocommerce_product_reviews_list_table', ob_get_clean(), $this->reviews_list_table ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}
}
Internal/Admin/ProductReviews/ReviewsListTable.php000064400000133165152333011040016277 0ustar00<?php
/**
 * Product > Reviews
 */

namespace Automattic\WooCommerce\Internal\Admin\ProductReviews;

use WC_Product;
use WP_Comment;
use WP_Comments_List_Table;
use WP_List_Table;
use WP_Post;

/**
 * Handles the Product Reviews page.
 */
class ReviewsListTable extends WP_List_Table {

	/**
	 * Memoization flag to determine if the current user can edit the current review.
	 *
	 * @var bool
	 */
	private $current_user_can_edit_review = false;

	/**
	 * Memoization flag to determine if the current user can moderate reviews.
	 *
	 * @var bool
	 */
	private $current_user_can_moderate_reviews;

	/**
	 * Current rating of reviews to display.
	 *
	 * @var int
	 */
	private $current_reviews_rating = 0;

	/**
	 * Current product the reviews should be displayed for.
	 *
	 * @var WC_Product|null Product or null for all products.
	 */
	private $current_product_for_reviews;

	/**
	 * Constructor.
	 *
	 * @param array|string $args Array or string of arguments.
	 */
	public function __construct( $args = [] ) {
		parent::__construct(
			wp_parse_args(
				$args,
				[
					'plural'   => 'product-reviews',
					'singular' => 'product-review',
				]
			)
		);

		$this->current_user_can_moderate_reviews = current_user_can( Reviews::get_capability( 'moderate' ) );
	}

	/**
	 * Prepares reviews for display.
	 *
	 * @return void
	 */
	public function prepare_items() : void {

		$this->set_review_status();
		$this->set_review_type();
		$this->current_reviews_rating = isset( $_REQUEST['review_rating'] ) ? absint( $_REQUEST['review_rating'] ) : 0;
		$this->set_review_product();

		$args = [
			'number'    => $this->get_per_page(),
			'post_type' => 'product',
		];

		// Include the order & orderby arguments.
		$args = wp_parse_args( $this->get_sort_arguments(), $args );
		// Handle the review item types filter.
		$args = wp_parse_args( $this->get_filter_type_arguments(), $args );
		// Handle the reviews rating filter.
		$args = wp_parse_args( $this->get_filter_rating_arguments(), $args );
		// Handle the review product filter.
		$args = wp_parse_args( $this->get_filter_product_arguments(), $args );
		// Include the review status arguments.
		$args = wp_parse_args( $this->get_status_arguments(), $args );
		// Include the search argument.
		$args = wp_parse_args( $this->get_search_arguments(), $args );
		// Include the offset argument.
		$args = wp_parse_args( $this->get_offset_arguments(), $args );

		/**
		 * Provides an opportunity to alter the comment query arguments used within
		 * the product reviews admin list table.
		 *
		 * @since 7.0.0
		 *
		 * @param array $args Comment query args.
		 */
		$args     = (array) apply_filters( 'woocommerce_product_reviews_list_table_prepare_items_args', $args );
		$comments = get_comments( $args );

		update_comment_cache( $comments );

		$this->items = $comments;

		$this->set_pagination_args(
			[
				'total_items' => get_comments( $this->get_total_comments_arguments( $args ) ),
				'per_page'    => $this->get_per_page(),
			]
		);
	}

	/**
	 * Returns the number of items to show per page.
	 *
	 * @return int Customized per-page value if available, or 20 as the default.
	 */
	protected function get_per_page() : int {
		return $this->get_items_per_page( 'edit_comments_per_page' );
	}

	/**
	 * Sets the product to filter reviews by.
	 *
	 * @return void
	 */
	protected function set_review_product() : void {

		$product_id = isset( $_REQUEST['product_id'] ) ? absint( $_REQUEST['product_id'] ) : null;
		$product = $product_id ? wc_get_product( $product_id ) : null;

		if ( $product instanceof WC_Product ) {
			$this->current_product_for_reviews = $product;
		}
	}

	/**
	 * Sets the `$comment_status` global based on the current request.
	 *
	 * @global string $comment_status
	 *
	 * @return void
	 */
	protected function set_review_status() : void {
		global $comment_status;

		$comment_status = sanitize_text_field( wp_unslash( $_REQUEST['comment_status'] ?? 'all' ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited

		if ( ! in_array( $comment_status, [ 'all', 'moderated', 'approved', 'spam', 'trash' ], true ) ) {
			$comment_status = 'all'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
		}
	}

	/**
	 * Sets the `$comment_type` global based on the current request.
	 *
	 * @global string $comment_type
	 *
	 * @return void
	 */
	protected function set_review_type() : void {
		global $comment_type;

		$review_type = sanitize_text_field( wp_unslash( $_REQUEST['review_type'] ?? 'all' ) );

		if ( 'all' !== $review_type && ! empty( $review_type ) ) {
			$comment_type = $review_type; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
		}
	}

	/**
	 * Builds the `orderby` and `order` arguments based on the current request.
	 *
	 * @return array
	 */
	protected function get_sort_arguments() : array {
		$orderby = sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ?? '' ) );
		$order   = sanitize_text_field( wp_unslash( $_REQUEST['order'] ?? '' ) );

		$args = [];

		if ( ! in_array( $orderby, $this->get_sortable_columns(), true ) ) {
			$orderby = 'comment_date_gmt';
		}

		// If ordering by "rating", then we need to adjust to sort by meta value.
		if ( 'rating' === $orderby ) {
			$orderby          = 'meta_value_num';
			$args['meta_key'] = 'rating';
		}

		if ( ! in_array( strtolower( $order ), [ 'asc', 'desc' ], true ) ) {
			$order = 'desc';
		}

		return wp_parse_args(
			[
				'orderby' => $orderby,
				'order'   => strtolower( $order ),
			],
			$args
		);
	}

	/**
	 * Builds the `type` argument based on the current request.
	 *
	 * @return array
	 */
	protected function get_filter_type_arguments() : array {

		$args      = [];
		$item_type = isset( $_REQUEST['review_type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['review_type'] ) ) : 'all';

		if ( 'all' === $item_type ) {
			return $args;
		}

		$args['type'] = $item_type;

		return $args;
	}

	/**
	 * Builds the `meta_query` arguments based on the current request.
	 *
	 * @return array
	 */
	protected function get_filter_rating_arguments() : array {

		$args = [];

		if ( empty( $this->current_reviews_rating ) ) {
			return $args;
		}

		$args['meta_query'] = [
			[
				'key'     => 'rating',
				'value'   => (int) $this->current_reviews_rating,
				'compare' => '=',
				'type'    => 'NUMERIC',
			],
		];

		return $args;
	}

	/**
	 * Gets the `post_id` argument based on the current request.
	 *
	 * @return array
	 */
	public function get_filter_product_arguments() : array {

		$args = [];

		if ( $this->current_product_for_reviews instanceof WC_Product ) {
			$args['post_id'] = $this->current_product_for_reviews->get_id();
		}

		return $args;
	}

	/**
	 * Gets the `status` argument based on the current request.
	 *
	 * @return array
	 */
	protected function get_status_arguments() : array {
		$args = [];

		global $comment_status;

		if ( ! empty( $comment_status ) && 'all' !== $comment_status && array_key_exists( $comment_status, $this->get_status_filters() ) ) {
			$args['status'] = $this->convert_status_to_query_value( $comment_status );
		}

		return $args;
	}

	/**
	 * Gets the `search` argument based on the current request.
	 *
	 * @return array
	 */
	protected function get_search_arguments() : array {
		$args = [];

		if ( ! empty( $_REQUEST['s'] ) ) {
			$args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) );
		}

		return $args;
	}

	/**
	 * Returns the `offset` argument based on the current request.
	 *
	 * @return array
	 */
	protected function get_offset_arguments() : array {
		$args = [];

		if ( isset( $_REQUEST['start'] ) ) {
			$args['offset'] = absint( wp_unslash( $_REQUEST['start'] ) );
		} else {
			$args['offset'] = ( $this->get_pagenum() - 1 ) * $this->get_per_page();
		}

		return $args;
	}

	/**
	 * Returns the arguments used to count the total number of comments.
	 *
	 * @param array $default_query_args Query args for the main request.
	 * @return array
	 */
	protected function get_total_comments_arguments( array $default_query_args ) : array {
		return wp_parse_args(
			[
				'count'  => true,
				'offset' => 0,
				'number' => 0,
			],
			$default_query_args
		);
	}

	/**
	 * Displays the product reviews HTML table.
	 *
	 * Reimplements {@see WP_Comment_::display()} but we change the ID to match the one output by {@see WP_Comments_List_Table::display()}.
	 * This will automatically handle additional CSS for consistency with the comments page.
	 *
	 * @return void
	 */
	public function display() : void {
		$this->display_tablenav( 'top' );

		$this->screen->render_screen_reader_content( 'heading_list' );

		?>
		<table class="wp-list-table <?php echo esc_attr( implode( ' ', $this->get_table_classes() ) ); ?>">
			<thead>
			<tr>
				<?php $this->print_column_headers(); ?>
			</tr>
			</thead>
			<tbody id="the-comment-list" data-wp-lists="list:comment">
			<?php $this->display_rows_or_placeholder(); ?>
			</tbody>
			<tfoot>
			<tr>
				<?php $this->print_column_headers( false ); ?>
			</tr>
			</tfoot>
		</table>
		<?php

		$this->display_tablenav( 'bottom' );
	}

	/**
	 * Render a single row HTML.
	 *
	 * @global WP_Post $post
	 * @global WP_Comment $comment
	 *
	 * @param WP_Comment|mixed $item Review or reply being rendered.
	 * @return void
	 */
	public function single_row( $item ) : void {
		global $post, $comment;

		// Overrides the comment global for properly rendering rows.
		$comment           = $item; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
		$the_comment_class = (string) wp_get_comment_status( $comment->comment_ID );
		$the_comment_class = implode( ' ', get_comment_class( $the_comment_class, $comment->comment_ID, $comment->comment_post_ID ) );
		// Sets the post for the product in context.
		$post = get_post( $comment->comment_post_ID ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited

		$this->current_user_can_edit_review = current_user_can( 'edit_comment', $comment->comment_ID );

		?>
		<tr id="comment-<?php echo esc_attr( $comment->comment_ID ); ?>" class="comment <?php echo esc_attr( $the_comment_class ); ?>">
			<?php $this->single_row_columns( $comment ); ?>
		</tr>
		<?php
	}

	/**
	 * Generate and display row actions links.
	 *
	 * @see WP_Comments_List_Table::handle_row_actions() for consistency.
	 *
	 * @global string $comment_status Status for the current listed comments.
	 *
	 * @param WP_Comment|mixed $item        The product review or reply in context.
	 * @param string|mixed     $column_name Current column name.
	 * @param string|mixed     $primary     Primary column name.
	 * @return string
	 */
	protected function handle_row_actions( $item, $column_name, $primary ) : string {
		global $comment_status;

		if ( $primary !== $column_name || ! $this->current_user_can_edit_review ) {
			return '';
		}

		$review_status = wp_get_comment_status( $item );

		$url = add_query_arg(
			[
				'c' => urlencode( $item->comment_ID ),
			],
			admin_url( 'comment.php' )
		);

		$approve_url   = wp_nonce_url( add_query_arg( 'action', 'approvecomment', $url ), "approve-comment_$item->comment_ID" );
		$unapprove_url = wp_nonce_url( add_query_arg( 'action', 'unapprovecomment', $url ), "approve-comment_$item->comment_ID" );
		$spam_url      = wp_nonce_url( add_query_arg( 'action', 'spamcomment', $url ), "delete-comment_$item->comment_ID" );
		$unspam_url    = wp_nonce_url( add_query_arg( 'action', 'unspamcomment', $url ), "delete-comment_$item->comment_ID" );
		$trash_url     = wp_nonce_url( add_query_arg( 'action', 'trashcomment', $url ), "delete-comment_$item->comment_ID" );
		$untrash_url   = wp_nonce_url( add_query_arg( 'action', 'untrashcomment', $url ), "delete-comment_$item->comment_ID" );
		$delete_url    = wp_nonce_url( add_query_arg( 'action', 'deletecomment', $url ), "delete-comment_$item->comment_ID" );

		$actions = [
			'approve'   => '',
			'unapprove' => '',
			'reply'     => '',
			'quickedit' => '',
			'edit'      => '',
			'spam'      => '',
			'unspam'    => '',
			'trash'     => '',
			'untrash'   => '',
			'delete'    => '',
		];

		if ( $comment_status && 'all' !== $comment_status ) {
			if ( 'approved' === $review_status ) {
				$actions['unapprove'] = sprintf(
					'<a href="%s" data-wp-lists="%s" class="vim-u vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
					esc_url( $unapprove_url ),
					esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}:e7e7d3:action=dim-comment&amp;new=unapproved" ),
					esc_attr__( 'Unapprove this review', 'woocommerce' ),
					esc_html__( 'Unapprove', 'woocommerce' )
				);
			} elseif ( 'unapproved' === $review_status ) {
				$actions['approve'] = sprintf(
					'<a href="%s" data-wp-lists="%s" class="vim-a vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
					esc_url( $approve_url ),
					esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}:e7e7d3:action=dim-comment&amp;new=approved" ),
					esc_attr__( 'Approve this review', 'woocommerce' ),
					esc_html__( 'Approve', 'woocommerce' )
				);
			}
		} else {
			$actions['approve'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="vim-a aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $approve_url ),
				esc_attr( "dim:the-comment-list:comment-{$item->comment_ID}:unapproved:e7e7d3:e7e7d3:new=approved" ),
				esc_attr__( 'Approve this review', 'woocommerce' ),
				esc_html__( 'Approve', 'woocommerce' )
			);

			$actions['unapprove'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="vim-u aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $unapprove_url ),
				esc_attr( "dim:the-comment-list:comment-{$item->comment_ID}:unapproved:e7e7d3:e7e7d3:new=unapproved" ),
				esc_attr__( 'Unapprove this review', 'woocommerce' ),
				esc_html__( 'Unapprove', 'woocommerce' )
			);
		}

		if ( 'spam' !== $review_status ) {
			$actions['spam'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="vim-s vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $spam_url ),
				esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}::spam=1" ),
				esc_attr__( 'Mark this review as spam', 'woocommerce' ),
				/* translators: "Mark as spam" link. */
				esc_html_x( 'Spam', 'verb', 'woocommerce' )
			);
		} else {
			$actions['unspam'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="vim-z vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $unspam_url ),
				esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}:66cc66:unspam=1" ),
				esc_attr__( 'Restore this review from the spam', 'woocommerce' ),
				esc_html_x( 'Not Spam', 'review', 'woocommerce' )
			);
		}

		if ( 'trash' === $review_status ) {
			$actions['untrash'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="vim-z vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $untrash_url ),
				esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}:66cc66:untrash=1" ),
				esc_attr__( 'Restore this review from the Trash', 'woocommerce' ),
				esc_html__( 'Restore', 'woocommerce' )
			);
		}

		if ( 'spam' === $review_status || 'trash' === $review_status || ! EMPTY_TRASH_DAYS ) {
			$actions['delete'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="delete vim-d vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $delete_url ),
				esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}::delete=1" ),
				esc_attr__( 'Delete this review permanently', 'woocommerce' ),
				esc_html__( 'Delete Permanently', 'woocommerce' )
			);
		} else {
			$actions['trash'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="delete vim-d vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $trash_url ),
				esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}::trash=1" ),
				esc_attr__( 'Move this review to the Trash', 'woocommerce' ),
				esc_html_x( 'Trash', 'verb', 'woocommerce' )
			);
		}

		if ( 'spam' !== $review_status && 'trash' !== $review_status ) {
			$actions['edit'] = sprintf(
				'<a href="%s" aria-label="%s">%s</a>',
				esc_url(
					add_query_arg(
						[
							'action' => 'editcomment',
							'c'      => urlencode( $item->comment_ID ),
						],
						admin_url( 'comment.php' )
					)
				),
				esc_attr__( 'Edit this review', 'woocommerce' ),
				esc_html__( 'Edit', 'woocommerce' )
			);

			$format = '<button type="button" data-comment-id="%d" data-post-id="%d" data-action="%s" class="%s button-link" aria-expanded="false" aria-label="%s">%s</button>';

			$actions['quickedit'] = sprintf(
				$format,
				esc_attr( $item->comment_ID ),
				esc_attr( $item->comment_post_ID ),
				'edit',
				'vim-q comment-inline',
				esc_attr__( 'Quick edit this review inline', 'woocommerce' ),
				esc_html__( 'Quick Edit', 'woocommerce' )
			);

			$actions['reply'] = sprintf(
				$format,
				esc_attr( $item->comment_ID ),
				esc_attr( $item->comment_post_ID ),
				'replyto',
				'vim-r comment-inline',
				esc_attr__( 'Reply to this review', 'woocommerce' ),
				esc_html__( 'Reply', 'woocommerce' )
			);
		}

		$always_visible = 'excerpt' === get_user_setting( 'posts_list_mode', 'list' );

		$output = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';

		$i = 0;

		foreach ( array_filter( $actions ) as $action => $link ) {
			++$i;

			if ( ( ( 'approve' === $action || 'unapprove' === $action ) && 2 === $i ) || 1 === $i ) {
				$sep = '';
			} else {
				$sep = ' | ';
			}

			if ( ( 'reply' === $action || 'quickedit' === $action ) && ! wp_doing_ajax() ) {
				$action .= ' hide-if-no-js';
			} elseif ( ( 'untrash' === $action && 'trash' === $review_status ) || ( 'unspam' === $action && 'spam' === $review_status ) ) {
				if ( '1' === get_comment_meta( $item->comment_ID, '_wp_trash_meta_status', true ) ) {
					$action .= ' approve';
				} else {
					$action .= ' unapprove';
				}
			}

			$output .= "<span class='$action'>$sep$link</span>";
		}

		$output .= '</div>';
		$output .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . esc_html__( 'Show more details', 'woocommerce' ) . '</span></button>';

		return $output;
	}

	/**
	 * Gets the columns for the table.
	 *
	 * @return array Table columns and their headings.
	 */
	public function get_columns() : array {
		$columns = [
			'cb'       => '<input type="checkbox" />',
			'type'     => _x( 'Type', 'review type', 'woocommerce' ),
			'author'   => __( 'Author', 'woocommerce' ),
			'rating'   => __( 'Rating', 'woocommerce' ),
			'comment'  => _x( 'Review', 'column name', 'woocommerce' ),
			'response' => __( 'Product', 'woocommerce' ),
			'date'     => _x( 'Submitted on', 'column name', 'woocommerce' ),
		];

		/**
		 * Filters the table columns.
		 *
		 * @since 6.7.0
		 *
		 * @param array $columns
		 */
		return (array) apply_filters( 'woocommerce_product_reviews_table_columns', $columns );
	}

	/**
	 * Gets the name of the default primary column.
	 *
	 * @return string Name of the primary colum.
	 */
	protected function get_primary_column_name() : string {
		return 'comment';
	}

	/**
	 * Gets a list of sortable columns.
	 *
	 * Key is the column ID and value is which database column we perform the sorting on.
	 * The `rating` column uses a unique key instead, as that requires sorting by meta value.
	 *
	 * @return array
	 */
	protected function get_sortable_columns() : array {
		return [
			'author'   => 'comment_author',
			'response' => 'comment_post_ID',
			'date'     => 'comment_date_gmt',
			'type'     => 'comment_type',
			'rating'   => 'rating',
		];
	}

	/**
	 * Returns a list of available bulk actions.
	 *
	 * @global string $comment_status
	 *
	 * @return array
	 */
	protected function get_bulk_actions() : array {
		global $comment_status;

		$actions = [];

		if ( in_array( $comment_status, [ 'all', 'approved' ], true ) ) {
			$actions['unapprove'] = __( 'Unapprove', 'woocommerce' );
		}

		if ( in_array( $comment_status, [ 'all', 'moderated' ], true ) ) {
			$actions['approve'] = __( 'Approve', 'woocommerce' );
		}

		if ( in_array( $comment_status, [ 'all', 'moderated', 'approved', 'trash' ], true ) ) {
			$actions['spam'] = _x( 'Mark as spam', 'review', 'woocommerce' );
		}

		if ( 'trash' === $comment_status ) {
			$actions['untrash'] = __( 'Restore', 'woocommerce' );
		} elseif ( 'spam' === $comment_status ) {
			$actions['unspam'] = _x( 'Not spam', 'review', 'woocommerce' );
		}

		if ( in_array( $comment_status, [ 'trash', 'spam' ], true ) || ! EMPTY_TRASH_DAYS ) {
			$actions['delete'] = __( 'Delete permanently', 'woocommerce' );
		} else {
			$actions['trash'] = __( 'Move to Trash', 'woocommerce' );
		}

		return $actions;
	}

	/**
	 * Returns the current action select in bulk actions menu.
	 *
	 * This is overridden in order to support `delete_all` for use in {@see ReviewsListTable::process_bulk_action()}
	 *
	 * {@see WP_Comments_List_Table::current_action()} for reference.
	 *
	 * @return string|false
	 */
	public function current_action() {
		if ( isset( $_REQUEST['delete_all'] ) || isset( $_REQUEST['delete_all2'] ) ) {
			return 'delete_all';
		}

		return parent::current_action();
	}

	/**
	 * Processes the bulk actions.
	 *
	 * @return void
	 */
	public function process_bulk_action() : void {

		if ( ! $this->current_user_can_moderate_reviews ) {
			return;
		}

		if ( $this->current_action() ) {
			check_admin_referer( 'bulk-product-reviews' );

			$query_string = remove_query_arg( [ 'page', '_wpnonce' ], wp_unslash( ( $_SERVER['QUERY_STRING'] ?? '' ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

			// Replace current nonce with bulk-comments nonce.
			$comments_nonce = wp_create_nonce( 'bulk-comments' );
			$query_string   = add_query_arg( '_wpnonce', $comments_nonce, $query_string );

			// Redirect to edit-comments.php, which will handle processing the action for us.
			wp_safe_redirect( esc_url_raw( admin_url( 'edit-comments.php?' . $query_string ) ) );
			exit;
		} elseif ( ! empty( $_GET['_wp_http_referer'] ) ) {

			wp_safe_redirect( remove_query_arg( [ '_wp_http_referer', '_wpnonce' ] ) );
			exit;
		}
	}

	/**
	 * Returns an array of supported statuses and their labels.
	 *
	 * @return array
	 */
	protected function get_status_filters() : array {
		return [
			/* translators: %s: Number of reviews. */
			'all'       => _nx_noop(
				'All <span class="count">(%s)</span>',
				'All <span class="count">(%s)</span>',
				'product reviews',
				'woocommerce'
			),
			/* translators: %s: Number of reviews. */
			'moderated' => _nx_noop(
				'Pending <span class="count">(%s)</span>',
				'Pending <span class="count">(%s)</span>',
				'product reviews',
				'woocommerce'
			),
			/* translators: %s: Number of reviews. */
			'approved'  => _nx_noop(
				'Approved <span class="count">(%s)</span>',
				'Approved <span class="count">(%s)</span>',
				'product reviews',
				'woocommerce'
			),
			/* translators: %s: Number of reviews. */
			'spam'      => _nx_noop(
				'Spam <span class="count">(%s)</span>',
				'Spam <span class="count">(%s)</span>',
				'product reviews',
				'woocommerce'
			),
			/* translators: %s: Number of reviews. */
			'trash'     => _nx_noop(
				'Trash <span class="count">(%s)</span>',
				'Trash <span class="count">(%s)</span>',
				'product reviews',
				'woocommerce'
			),
		];
	}

	/**
	 * Returns the available status filters.
	 *
	 * @see WP_Comments_List_Table::get_views() for consistency.
	 *
	 * @global int    $post_id
	 * @global string $comment_status
	 * @global string $comment_type
	 *
	 * @return array An associative array of fully-formed comment status links. Includes 'All', 'Pending', 'Approved', 'Spam', and 'Trash'.
	 */
	protected function get_views() : array {
		global $post_id, $comment_status, $comment_type;

		$status_links = [];

		$status_labels = $this->get_status_filters();

		if ( ! EMPTY_TRASH_DAYS ) {
			unset( $status_labels['trash'] );
		}

		$link = $this->get_view_url( (string) $comment_type, (int) $post_id );

		foreach ( $status_labels as $status => $label ) {
			$current_link_attributes = '';

			if ( $status === $comment_status ) {
				$current_link_attributes = ' class="current" aria-current="page"';
			}

			$link = add_query_arg( 'comment_status', urlencode( $status ), $link );

			$number_reviews_for_status = $this->get_review_count( $status, (int) $post_id );

			$count_html = sprintf(
				'<span class="%s-count">%s</span>',
				( 'moderated' === $status ) ? 'pending' : $status,
				number_format_i18n( $number_reviews_for_status )
			);

			$status_links[ $status ] = '<a href="' . esc_url( $link ) . '"' . $current_link_attributes . '>' . sprintf( translate_nooped_plural( $label, $number_reviews_for_status ), $count_html ) . '</a>';
		}

		return $status_links;
	}

	/**
	 * Gets the base URL for a view, excluding the status (that should be appended).
	 *
	 * @param string $comment_type Comment type filter.
	 * @param int    $post_id      Current post ID.
	 * @return string
	 */
	protected function get_view_url( string $comment_type, int $post_id ) : string {
		$link = Reviews::get_reviews_page_url();

		if ( ! empty( $comment_type ) && 'all' !== $comment_type ) {
			$link = add_query_arg( 'comment_type', urlencode( $comment_type ), $link );
		}
		if ( ! empty( $post_id ) ) {
			$link = add_query_arg( 'p', absint( $post_id ), $link );
		}

		return $link;
	}

	/**
	 * Gets the number of reviews (including review replies) for a given status.
	 *
	 * @param string $status     Status key from {@see ReviewsListTable::get_status_filters()}.
	 * @param int    $product_id ID of the product if we're filtering by product in this request. Otherwise, `0` for no product filters.
	 * @return int
	 */
	protected function get_review_count( string $status, int $product_id ) : int {
		return (int) get_comments(
			[
				'type__in'  => [ 'review', 'comment' ],
				'status'    => $this->convert_status_to_query_value( $status ),
				'post_type' => 'product',
				'post_id'   => $product_id,
				'count'     => true,
			]
		);
	}

	/**
	 * Converts a status key into its equivalent `comment_approved` database column value.
	 *
	 * @param string $status Status key from {@see ReviewsListTable::get_status_filters()}.
	 * @return string
	 */
	protected function convert_status_to_query_value( string $status ) : string {
		// These keys exactly match the database column.
		if ( in_array( $status, [ 'spam', 'trash' ], true ) ) {
			return $status;
		}

		switch ( $status ) {
			case 'moderated':
				return '0';
			case 'approved':
				return '1';
			default:
				return 'all';
		}
	}

	/**
	 * Outputs the text to display when there are no reviews to display.
	 *
	 * @see WP_List_Table::no_items()
	 *
	 * @global string $comment_status
	 *
	 * @return void
	 */
	public function no_items() : void {
		global $comment_status;

		if ( 'moderated' === $comment_status ) {
			esc_html_e( 'No reviews awaiting moderation.', 'woocommerce' );
		} else {
			esc_html_e( 'No reviews found.', 'woocommerce' );
		}
	}

	/**
	 * Renders the checkbox column.
	 *
	 * @param WP_Comment|mixed $item Review or reply being rendered.
	 * @return void
	 */
	protected function column_cb( $item ) : void {

		ob_start();

		if ( $this->current_user_can_edit_review ) {
			?>
			<label class="screen-reader-text" for="cb-select-<?php echo esc_attr( $item->comment_ID ); ?>"><?php esc_html_e( 'Select review', 'woocommerce' ); ?></label>
			<input
				id="cb-select-<?php echo esc_attr( $item->comment_ID ); ?>"
				type="checkbox"
				name="delete_comments[]"
				value="<?php echo esc_attr( $item->comment_ID ); ?>"
			/>
			<?php
		}

		echo $this->filter_column_output( 'cb', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Renders the review column.
	 *
	 * @see WP_Comments_List_Table::column_comment() for consistency.
	 *
	 * @param WP_Comment|mixed $item Review or reply being rendered.
	 * @return void
	 */
	protected function column_comment( $item ) : void {

		$in_reply_to = $this->get_in_reply_to_review_text( $item );

		ob_start();

		if ( $in_reply_to ) {
			echo $in_reply_to . '<br><br>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		printf(
			'%1$s%2$s%3$s',
			'<div class="comment-text">',
			get_comment_text( $item->comment_ID ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			'</div>'
		);

		if ( $this->current_user_can_edit_review ) {
			?>
			<div id="inline-<?php echo esc_attr( $item->comment_ID ); ?>" class="hidden">
				<textarea class="comment" rows="1" cols="1"><?php echo esc_textarea( $item->comment_content ); ?></textarea>
				<div class="author-email"><?php echo esc_attr( $item->comment_author_email ); ?></div>
				<div class="author"><?php echo esc_attr( $item->comment_author ); ?></div>
				<div class="author-url"><?php echo esc_attr( $item->comment_author_url ); ?></div>
				<div class="comment_status"><?php echo esc_html( $item->comment_approved ); ?></div>
			</div>
			<?php
		}

		echo $this->filter_column_output( 'comment', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Gets the in-reply-to-review text.
	 *
	 * @param WP_Comment|mixed $reply Reply to review.
	 * @return string
	 */
	private function get_in_reply_to_review_text( $reply ) : string {

		$review = $reply->comment_parent ? get_comment( $reply->comment_parent ) : null;

		if ( ! $review ) {
			return '';
		}

		$parent_review_link = get_comment_link( $review );
		$review_author_name = get_comment_author( $review );

		return sprintf(
			/* translators: %s: Parent review link with review author name. */
			ent2ncr( __( 'In reply to %s.', 'woocommerce' ) ),
			'<a href="' . esc_url( $parent_review_link ) . '">' . esc_html( $review_author_name ) . '</a>'
		);
	}

	/**
	 * Renders the author column.
	 *
	 * @see WP_Comments_List_Table::column_author() for consistency.
	 *
	 * @param WP_Comment|mixed $item Review or reply being rendered.
	 * @return void
	 */
	protected function column_author( $item ) : void {
		global $comment_status;

		$author_url = $this->get_item_author_url();
		$author_url_display = $this->get_item_author_url_for_display( $author_url );

		if ( get_option( 'show_avatars' ) ) {
			$author_avatar = get_avatar( $item, 32, 'mystery' );
		} else {
			$author_avatar = '';
		}

		ob_start();

		echo '<strong>' . $author_avatar; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		comment_author();
		echo '</strong><br>';

		if ( ! empty( $author_url ) ) :

			?>
			<a title="<?php echo esc_attr( $author_url ); ?>" href="<?php echo esc_url( $author_url ); ?>" rel="noopener noreferrer"><?php echo esc_html( $author_url_display ); ?></a>
			<br>
			<?php

		endif;

		if ( $this->current_user_can_edit_review ) :

			if ( ! empty( $item->comment_author_email ) && is_email( $item->comment_author_email ) ) :

				?>
				<a href="mailto:<?php echo esc_attr( $item->comment_author_email ); ?>"><?php echo esc_html( $item->comment_author_email ); ?></a>
				<?php

			endif;

			$link = add_query_arg(
				[
					's'    => urlencode( get_comment_author_IP( $item->comment_ID ) ),
					'page' => Reviews::MENU_SLUG,
					'mode' => 'detail',
				],
				'admin.php'
			);

			if ( 'spam' === $comment_status ) :
				$link = add_query_arg( [ 'comment_status' => 'spam' ], $link );
			endif;

			?>
			<a href="<?php echo esc_url( $link ); ?>"><?php comment_author_IP( $item->comment_ID ); ?></a>
			<?php

		endif;

		echo $this->filter_column_output( 'author', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Gets the item author URL.
	 *
	 * @return string
	 */
	private function get_item_author_url() : string {

		$author_url = get_comment_author_url();
		$protocols = [ 'https://', 'http://' ];

		if ( in_array( $author_url, $protocols ) ) {
			$author_url = '';
		}

		return $author_url;
	}

	/**
	 * Gets the item author URL for display.
	 *
	 * @param string $author_url The review or reply author URL (raw).
	 * @return string
	 */
	private function get_item_author_url_for_display( $author_url ) : string {

		$author_url_display = untrailingslashit( preg_replace( '|^http(s)?://(www\.)?|i', '', $author_url ) );

		if ( strlen( $author_url_display ) > 50 ) {
			$author_url_display = wp_html_excerpt( $author_url_display, 49, '&hellip;' );
		}

		return $author_url_display;
	}

	/**
	 * Renders the "submitted on" column.
	 *
	 * Note that the output is consistent with {@see WP_Comments_List_Table::column_date()}.
	 *
	 * @param WP_Comment|mixed $item Review or reply being rendered.
	 * @return void
	 */
	protected function column_date( $item ) : void {

		$submitted = sprintf(
			/* translators: 1 - Product review date, 2: Product review time. */
			__( '%1$s at %2$s', 'woocommerce' ),
			/* translators: Review date format. See https://www.php.net/manual/datetime.format.php */
			get_comment_date( __( 'Y/m/d', 'woocommerce' ), $item ),
			/* translators: Review time format. See https://www.php.net/manual/datetime.format.php */
			get_comment_date( __( 'g:i a', 'woocommerce' ), $item )
		);

		ob_start();

		?>
		<div class="submitted-on">
			<?php

			if ( 'approved' === wp_get_comment_status( $item ) && ! empty( $item->comment_post_ID ) ) :
				printf(
					'<a href="%1$s">%2$s</a>',
					esc_url( get_comment_link( $item ) ),
					esc_html( $submitted )
				);
			else :
				echo esc_html( $submitted );
			endif;

			?>
		</div>
		<?php

		echo $this->filter_column_output( 'date', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Renders the product column.
	 *
	 * @see WP_Comments_List_Table::column_response() for consistency.
	 *
	 * @param WP_Comment|mixed $item Review or reply being rendered.
	 * @return void
	 */
	protected function column_response( $item ) : void {
		$product_post = get_post();

		ob_start();

		if ( $product_post ) :

			?>
			<div class="response-links">
				<?php

				if ( current_user_can( 'edit_product', $product_post->ID ) ) :
					$post_link  = "<a href='" . esc_url( get_edit_post_link( $product_post->ID ) ) . "' class='comments-edit-item-link'>";
					$post_link .= esc_html( get_the_title( $product_post->ID ) ) . '</a>';
				else :
					$post_link = esc_html( get_the_title( $product_post->ID ) );
				endif;

				echo $post_link; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

				$post_type_object = get_post_type_object( $product_post->post_type );

				?>
				<a href="<?php echo esc_url( get_permalink( $product_post->ID ) ); ?>" class="comments-view-item-link">
					<?php echo esc_html( $post_type_object->labels->view_item ); ?>
				</a>
				<span class="post-com-count-wrapper post-com-count-<?php echo esc_attr( $product_post->ID ); ?>">
					<?php $this->comments_bubble( $product_post->ID, get_pending_comments_num( $product_post->ID ) ); ?>
				</span>
			</div>
			<?php

		endif;

		echo $this->filter_column_output( 'response', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Renders the type column.
	 *
	 * @param WP_Comment|mixed $item Review or reply being rendered.
	 * @return void
	 */
	protected function column_type( $item ) : void {

		$type = 'review' === $item->comment_type
			? '&#9734;&nbsp;' . __( 'Review', 'woocommerce' )
			: __( 'Reply', 'woocommerce' );

		echo $this->filter_column_output( 'type', esc_html( $type ), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Renders the rating column.
	 *
	 * @param WP_Comment|mixed $item Review or reply being rendered.
	 * @return void
	 */
	protected function column_rating( $item ) : void {
		$rating = get_comment_meta( $item->comment_ID, 'rating', true );

		ob_start();

		if ( ! empty( $rating ) && is_numeric( $rating ) ) {
			$rating = (int) $rating;

			$accessibility_label = sprintf(
				/* translators: 1: number representing a rating */
				__( '%1$d out of 5', 'woocommerce' ),
				$rating
			);

			$stars = str_repeat( '&#9733;', $rating );
			$stars .= str_repeat( '&#9734;', 5 - $rating );

			?>
			<span aria-label="<?php echo esc_attr( $accessibility_label ); ?>"><?php echo esc_html( $stars ); ?></span>
			<?php
		}

		echo $this->filter_column_output( 'rating', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Renders any custom columns.
	 *
	 * @param WP_Comment|mixed $item        Review or reply being rendered.
	 * @param string|mixed     $column_name Name of the column being rendered.
	 * @return void
	 */
	protected function column_default( $item, $column_name ) : void {

		ob_start();

		/**
		 * Fires when the default column output is displayed for a single row.
		 *
		 * This action can be used to render custom columns that have been added.
		 *
		 * @since 6.7.0
		 *
		 * @param WP_Comment $item The review or reply being rendered.
		 */
		do_action( 'woocommerce_product_reviews_table_column_' . $column_name, $item );

		echo $this->filter_column_output( $column_name, ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Runs a filter hook for a given column content.
	 *
	 * @param string|mixed     $column_name The column being output.
	 * @param string|mixed     $output      The output content (may include HTML).
	 * @param WP_Comment|mixed $item        The review or reply being rendered.
	 * @return string
	 */
	protected function filter_column_output( $column_name, $output, $item ) : string {

		/**
		 * Filters the output of a column.
		 *
		 * @since 6.7.0
		 *
		 * @param string     $output The column output.
		 * @param WP_Comment $item   The product review being rendered.
		 */
		return (string) apply_filters( 'woocommerce_product_reviews_table_column_' . $column_name . '_content', $output, $item );
	}

	/**
	 * Renders the extra controls to be displayed between bulk actions and pagination.
	 *
	 * @global string $comment_status
	 * @global string $comment_type
	 *
	 * @param string|mixed $which Position (top or bottom).
	 * @return void
	 */
	protected function extra_tablenav( $which ) : void {
		global $comment_status, $comment_type;

		echo '<div class="alignleft actions">';

		if ( 'top' === $which ) {

			ob_start();

			echo '<input type="hidden" name="comment_status" value="' . esc_attr( $comment_status ?? 'all' ) . '" />';

			$this->review_type_dropdown( $comment_type );
			$this->review_rating_dropdown( $this->current_reviews_rating );
			$this->product_search( $this->current_product_for_reviews );

			echo ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

			submit_button( __( 'Filter', 'woocommerce' ), '', 'filter_action', false, [ 'id' => 'post-query-submit' ] );
		}

		if ( ( 'spam' === $comment_status || 'trash' === $comment_status ) && $this->has_items() && $this->current_user_can_moderate_reviews ) {

			wp_nonce_field( 'bulk-destroy', '_destroy_nonce' );

			$title = 'spam' === $comment_status
				? esc_attr__( 'Empty Spam', 'woocommerce' )
				: esc_attr__( 'Empty Trash', 'woocommerce' );

			submit_button( $title, 'apply', 'delete_all', false );
		}

		echo '</div>';
	}

	/**
	 * Displays a review type drop-down for filtering reviews in the Product Reviews list table.
	 *
	 * @see WP_Comments_List_Table::comment_type_dropdown() for consistency.
	 *
	 * @param string|mixed $current_type The current comment item type slug.
	 * @return void
	 */
	protected function review_type_dropdown( $current_type ) : void {
		/**
		 * Sets the possible options used in the Product Reviews List Table's filter-by-review-type
		 * selector.
		 *
		 * @since 7.0.0
		 *
		 * @param array Map of possible review types.
		 */
		$item_types = apply_filters(
			'woocommerce_product_reviews_list_table_item_types',
			array(
				'all'     => __( 'All types', 'woocommerce' ),
				'comment' => __( 'Replies', 'woocommerce' ),
				'review'  => __( 'Reviews', 'woocommerce' ),
			)
		);

		?>
		<label class="screen-reader-text" for="filter-by-review-type"><?php esc_html_e( 'Filter by review type', 'woocommerce' ); ?></label>
		<select id="filter-by-review-type" name="review_type">
			<?php foreach ( $item_types as $type => $label ) : ?>
				<option value="<?php echo esc_attr( $type ); ?>" <?php selected( $type, $current_type ); ?>><?php echo esc_html( $label ); ?></option>
			<?php endforeach; ?>
		</select>
		<?php
	}

	/**
	 * Displays a review rating drop-down for filtering reviews in the Product Reviews list table.
	 *
	 * @param int|string|mixed $current_rating Rating to display reviews for.
	 * @return void
	 */
	public function review_rating_dropdown( $current_rating ) : void {

		$rating_options = [
			'0' => __( 'All ratings', 'woocommerce' ),
			'1' => '&#9733;',
			'2' => '&#9733;&#9733;',
			'3' => '&#9733;&#9733;&#9733;',
			'4' => '&#9733;&#9733;&#9733;&#9733;',
			'5' => '&#9733;&#9733;&#9733;&#9733;&#9733;',
		];

		?>
		<label class="screen-reader-text" for="filter-by-review-rating"><?php esc_html_e( 'Filter by review rating', 'woocommerce' ); ?></label>
		<select id="filter-by-review-rating" name="review_rating">
			<?php foreach ( $rating_options as $rating => $label ) : ?>
				<?php

				$title = 0 === (int) $rating
					? $label
					: sprintf(
						/* translators: %s: Star rating (1-5). */
						__( '%s-star rating', 'woocommerce' ),
						$rating
					);

				?>
				<option value="<?php echo esc_attr( $rating ); ?>" <?php selected( $rating, (string) $current_rating ); ?> title="<?php echo esc_attr( $title ); ?>"><?php echo esc_html( $label ); ?></option>
			<?php endforeach; ?>
		</select>
		<?php
	}

	/**
	 * Displays a product search input for filtering reviews by product in the Product Reviews list table.
	 *
	 * @param WC_Product|null $current_product The current product (or null when displaying all reviews).
	 * @return void
	 */
	protected function product_search( ?WC_Product $current_product ) : void {
		?>
		<label class="screen-reader-text" for="filter-by-product"><?php esc_html_e( 'Filter by product', 'woocommerce' ); ?></label>
		<select
			id="filter-by-product"
			class="wc-product-search"
			name="product_id"
			style="width: 200px;"
			data-placeholder="<?php esc_attr_e( 'Search for a product&hellip;', 'woocommerce' ); ?>"
			data-action="woocommerce_json_search_products"
			data-allow_clear="true">
			<?php if ( $current_product instanceof WC_Product ) : ?>
				<option value="<?php echo esc_attr( $current_product->get_id() ); ?>" selected="selected"><?php echo esc_html( $current_product->get_formatted_name() ); ?></option>
			<?php endif; ?>
		</select>
		<?php
	}

	/**
	 * Displays a review count bubble.
	 *
	 * Based on {@see WP_List_Table::comments_bubble()}, but overridden, so we can customize the URL and text output.
	 *
	 * @param int|mixed $post_id          The product ID.
	 * @param int|mixed $pending_comments Number of pending reviews.
	 *
	 * @return void
	 */
	protected function comments_bubble( $post_id, $pending_comments ) : void {
		$approved_review_count = get_comments_number();

		$approved_reviews_number = number_format_i18n( $approved_review_count );
		$pending_reviews_number  = number_format_i18n( $pending_comments );

		$approved_only_phrase = sprintf(
			/* translators: %s: Number of reviews. */
			_n( '%s review', '%s reviews', $approved_review_count, 'woocommerce' ),
			$approved_reviews_number
		);

		$approved_phrase = sprintf(
			/* translators: %s: Number of reviews. */
			_n( '%s approved review', '%s approved reviews', $approved_review_count, 'woocommerce' ),
			$approved_reviews_number
		);

		$pending_phrase = sprintf(
			/* translators: %s: Number of reviews. */
			_n( '%s pending review', '%s pending reviews', $pending_comments, 'woocommerce' ),
			$pending_reviews_number
		);

		if ( ! $approved_review_count && ! $pending_comments ) {
			// No reviews at all.
			printf(
				'<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">%s</span>',
				esc_html__( 'No reviews', 'woocommerce' )
			);
		} elseif ( $approved_review_count && 'trash' === get_post_status( $post_id ) ) {
			// Don't link the comment bubble for a trashed product.
			printf(
				'<span class="post-com-count post-com-count-approved"><span class="comment-count-approved" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
				esc_html( $approved_reviews_number ),
				$pending_comments ? esc_html( $approved_phrase ) : esc_html( $approved_only_phrase )
			);
		} elseif ( $approved_review_count ) {
			// Link the comment bubble to approved reviews.
			printf(
				'<a href="%s" class="post-com-count post-com-count-approved"><span class="comment-count-approved" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
				esc_url(
					add_query_arg(
						[
							'product_id'     => urlencode( $post_id ),
							'comment_status' => 'approved',
						],
						Reviews::get_reviews_page_url()
					)
				),
				esc_html( $approved_reviews_number ),
				$pending_comments ? esc_html( $approved_phrase ) : esc_html( $approved_only_phrase )
			);
		} else {
			// Don't link the comment bubble when there are no approved reviews.
			printf(
				'<span class="post-com-count post-com-count-no-comments"><span class="comment-count comment-count-no-comments" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
				esc_html( $approved_reviews_number ),
				$pending_comments ? esc_html__( 'No approved reviews', 'woocommerce' ) : esc_html__( 'No reviews', 'woocommerce' )
			);
		}

		if ( $pending_comments ) {
			printf(
				'<a href="%s" class="post-com-count post-com-count-pending"><span class="comment-count-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
				esc_url(
					add_query_arg(
						[
							'product_id'     => urlencode( $post_id ),
							'comment_status' => 'moderated',
						],
						Reviews::get_reviews_page_url()
					)
				),
				esc_html( $pending_reviews_number ),
				esc_html( $pending_phrase )
			);
		} else {
			printf(
				'<span class="post-com-count post-com-count-pending post-com-count-no-pending"><span class="comment-count comment-count-no-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
				esc_html( $pending_reviews_number ),
				$approved_review_count ? esc_html__( 'No pending reviews', 'woocommerce' ) : esc_html__( 'No reviews', 'woocommerce' )
			);
		}
	}

}
Internal/Admin/ProductReviews/ReviewsUtil.php000064400000001535152333011040015324 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Admin\ProductReviews;

/**
 * A utility class for handling comments that are product reviews.
 */
class ReviewsUtil {

	/**
	 * Removes product reviews from the edit-comments page to fix the "Mine" tab counter.
	 *
	 * @param  array|mixed $clauses A compacted array of comment query clauses.
	 * @return array|mixed
	 */
	public static function comments_clauses_without_product_reviews( $clauses ) {
		global $wpdb, $current_screen;

		if ( isset( $current_screen->base ) && 'edit-comments' === $current_screen->base ) {
			$clauses['join']  .= " LEFT JOIN {$wpdb->posts} AS wp_posts_to_exclude_reviews ON comment_post_ID = wp_posts_to_exclude_reviews.ID ";
			$clauses['where'] .= ( $clauses['where'] ? ' AND ' : '' ) . " wp_posts_to_exclude_reviews.post_type NOT IN ('product') ";
		}

		return $clauses;
	}
}
Internal/Admin/WCAdminSharedSettings.php000064400000003006152333011040014202 0ustar00<?php
/**
 * Manages the WC Admin settings that need to be pre-loaded.
 */

namespace Automattic\WooCommerce\Internal\Admin;

defined( 'ABSPATH' ) || exit;

/**
 * \Automattic\WooCommerce\Internal\Admin\WCAdminSharedSettings class.
 */
class WCAdminSharedSettings {
	/**
	 * Settings prefix used for the window.wcSettings object.
	 *
	 * @var string
	 */
	private $settings_prefix = 'admin';

	/**
	 * Class instance.
	 *
	 * @var WCAdminSharedSettings instance
	 */
	protected static $instance = null;

	/**
	 * Hook into WooCommerce Blocks.
	 */
	protected function __construct() {
		if ( did_action( 'woocommerce_blocks_loaded' ) ) {
			$this->on_woocommerce_blocks_loaded();
		} else {
			add_action( 'woocommerce_blocks_loaded', array( $this, 'on_woocommerce_blocks_loaded' ), 10 );
		}
	}

	/**
	 * Get class instance.
	 *
	 * @return object Instance.
	 */
	public static function get_instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Adds settings to the Blocks AssetDataRegistry when woocommerce_blocks is loaded.
	 *
	 * @return void
	 */
	public function on_woocommerce_blocks_loaded() {
		if ( class_exists( '\Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry' ) ) {
			\Automattic\WooCommerce\Blocks\Package::container()->get( \Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry::class )->add(
				$this->settings_prefix,
				function() {
					return apply_filters( 'woocommerce_admin_shared_settings', array() );
				},
				true
			);
		}
	}
}

Internal/Admin/WCAdminAssets.php000064400000027652152333011040012532 0ustar00<?php
/**
 * Register the scripts, and styles used within WooCommerce Admin.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use \_WP_Dependency;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Internal\Admin\Loader;

/**
 * WCAdminAssets Class.
 */
class WCAdminAssets {

	/**
	 * Class instance.
	 *
	 * @var WCAdminAssets instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor.
	 * Hooks added here should be removed in `wc_admin_initialize` via the feature plugin.
	 */
	public function __construct() {
		Features::get_instance();
		add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) );

		add_action( 'admin_enqueue_scripts', array( $this, 'inject_wc_settings_dependencies' ), 14 );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ), 15 );
	}

	/**
	 * Gets the path for the asset depending on file type.
	 *
	 * @param  string $ext File extension.
	 * @return string Folder path of asset.
	 */
	public static function get_path( $ext ) {
		return ( $ext === 'css' ) ? WC_ADMIN_DIST_CSS_FOLDER : WC_ADMIN_DIST_JS_FOLDER;
	}

	/**
	 * Determines if a minified JS file should be served.
	 *
	 * @param  boolean $script_debug Only serve unminified files if script debug is on.
	 * @return boolean If js asset should use minified version.
	 */
	public static function should_use_minified_js_file( $script_debug ) {
		// minified files are only shipped in non-core versions of wc-admin, return false if minified files are not available.
		if ( ! Features::exists( 'minified-js' ) ) {
			return false;
		}

		// Otherwise we will serve un-minified files if SCRIPT_DEBUG is on, or if anything truthy is passed in-lieu of SCRIPT_DEBUG.
		return ! $script_debug;
	}

	/**
	 * Gets the URL to an asset file.
	 *
	 * @param  string $file File name (without extension).
	 * @param  string $ext File extension.
	 * @return string URL to asset.
	 */
	public static function get_url( $file, $ext ) {
		$suffix = '';

		// Potentially enqueue minified JavaScript.
		if ( $ext === 'js' ) {
			$script_debug = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;
			$suffix       = self::should_use_minified_js_file( $script_debug ) ? '.min' : '';
		}

		return plugins_url( self::get_path( $ext ) . $file . $suffix . '.' . $ext, WC_ADMIN_PLUGIN_FILE );
	}

	/**
	 * Gets the file modified time as a cache buster if we're in dev mode, or the plugin version otherwise.
	 *
	 * @param string $ext File extension.
	 * @return string The cache buster value to use for the given file.
	 */
	public static function get_file_version( $ext ) {
		if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
			return filemtime( WC_ADMIN_ABSPATH . self::get_path( $ext ) );
		}
		return WC_VERSION;
	}

	/**
	 * Gets a script asset registry filename. The asset registry lists dependencies for the given script.
	 *
	 * @param  string $script_path_name Path to where the script asset registry is contained.
	 * @param  string $file File name (without extension).
	 * @return string complete asset filename.
	 *
	 * @throws \Exception Throws an exception when a readable asset registry file cannot be found.
	 */
	public static function get_script_asset_filename( $script_path_name, $file ) {
		$minification_supported = Features::exists( 'minified-js' );
		$script_min_filename    = $file . '.min.asset.php';
		$script_nonmin_filename = $file . '.asset.php';
		$script_asset_path      = WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . $script_path_name . '/';

		// Check minification is supported first, to avoid multiple is_readable checks when minification is
		// not supported.
		if ( $minification_supported && is_readable( $script_asset_path . $script_min_filename ) ) {
			return $script_min_filename;
		} elseif ( is_readable( $script_asset_path . $script_nonmin_filename ) ) {
			return $script_nonmin_filename;
		} else {
			// could not find an asset file, throw an error.
			throw new \Exception( 'Could not find asset registry for ' . $script_path_name );
		}
	}

	/**
	 * Render a preload link tag for a dependency, optionally
	 * checked against a provided allowlist.
	 *
	 * See: https://macarthur.me/posts/preloading-javascript-in-wordpress
	 *
	 * @param WP_Dependency $dependency The WP_Dependency being preloaded.
	 * @param string        $type Dependency type - 'script' or 'style'.
	 * @param array         $allowlist Optional. List of allowed dependency handles.
	 */
	private function maybe_output_preload_link_tag( $dependency, $type, $allowlist = array() ) {
		if (
			(
				! empty( $allowlist ) &&
				! in_array( $dependency->handle, $allowlist, true )
			) ||
			( ! empty( $this->preloaded_dependencies[ $type ] ) &&
			in_array( $dependency->handle, $this->preloaded_dependencies[ $type ], true ) )
		) {
			return;
		}

		$this->preloaded_dependencies[ $type ][] = $dependency->handle;

		$source = $dependency->ver ? add_query_arg( 'ver', $dependency->ver, $dependency->src ) : $dependency->src;

		echo '<link rel="preload" href="', esc_url( $source ), '" as="', esc_attr( $type ), '" />', "\n";
	}

	/**
	 * Output a preload link tag for dependencies (and their sub dependencies)
	 * with an optional allowlist.
	 *
	 * See: https://macarthur.me/posts/preloading-javascript-in-wordpress
	 *
	 * @param string $type Dependency type - 'script' or 'style'.
	 * @param array  $allowlist Optional. List of allowed dependency handles.
	 */
	private function output_header_preload_tags_for_type( $type, $allowlist = array() ) {
		if ( $type === 'script' ) {
			$dependencies_of_type = wp_scripts();
		} elseif ( $type === 'style' ) {
			$dependencies_of_type = wp_styles();
		} else {
			return;
		}

		foreach ( $dependencies_of_type->queue as $dependency_handle ) {
			$dependency = $dependencies_of_type->query( $dependency_handle, 'registered' );

			if ( $dependency === false ) {
				continue;
			}

			// Preload the subdependencies first.
			foreach ( $dependency->deps as $sub_dependency_handle ) {
				$sub_dependency = $dependencies_of_type->query( $sub_dependency_handle, 'registered' );

				if ( $sub_dependency ) {
					$this->maybe_output_preload_link_tag( $sub_dependency, $type, $allowlist );
				}
			}

			$this->maybe_output_preload_link_tag( $dependency, $type, $allowlist );
		}
	}

	/**
	 * Output preload link tags for all enqueued stylesheets and scripts.
	 *
	 * See: https://macarthur.me/posts/preloading-javascript-in-wordpress
	 */
	private function output_header_preload_tags() {
		$wc_admin_scripts = array(
			WC_ADMIN_APP,
			'wc-components',
		);

		$wc_admin_styles = array(
			WC_ADMIN_APP,
			'wc-components',
			'wc-material-icons',
		);

		// Preload styles.
		$this->output_header_preload_tags_for_type( 'style', $wc_admin_styles );

		// Preload scripts.
		$this->output_header_preload_tags_for_type( 'script', $wc_admin_scripts );
	}

	/**
	 * Loads the required scripts on the correct pages.
	 */
	public function enqueue_assets() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}

		wp_enqueue_script( WC_ADMIN_APP );
		wp_enqueue_style( WC_ADMIN_APP );
		wp_enqueue_style( 'wc-material-icons' );
		wp_enqueue_style( 'wc-onboarding' );

		// Preload our assets.
		$this->output_header_preload_tags();
	}

	/**
	 * Registers all the neccessary scripts and styles to show the admin experience.
	 */
	public function register_scripts() {
		if ( ! function_exists( 'wp_set_script_translations' ) ) {
			return;
		}

		$js_file_version  = self::get_file_version( 'js' );
		$css_file_version = self::get_file_version( 'css' );

		$scripts = array(
			'wc-explat',
			'wc-experimental',
			'wc-customer-effort-score',
			// NOTE: This should be removed when Gutenberg is updated and the notices package is removed from WooCommerce Admin.
			'wc-notices',
			'wc-number',
			'wc-tracks',
			'wc-date',
			'wc-components',
			WC_ADMIN_APP,
			'wc-csv',
			'wc-store-data',
			'wc-currency',
			'wc-navigation',
		);

		$scripts_map = array(
			WC_ADMIN_APP    => 'app',
			'wc-csv'        => 'csv-export',
			'wc-store-data' => 'data',
		);

		$translated_scripts = array(
			'wc-currency',
			'wc-date',
			'wc-components',
			'wc-customer-effort-score',
			WC_ADMIN_APP,
		);

		foreach ( $scripts as $script ) {
			$script_path_name = isset( $scripts_map[ $script ] ) ? $scripts_map[ $script ] : str_replace( 'wc-', '', $script );

			try {
				$script_assets_filename = self::get_script_asset_filename( $script_path_name, 'index' );
				$script_assets          = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . $script_path_name . '/' . $script_assets_filename;

				wp_register_script(
					$script,
					self::get_url( $script_path_name . '/index', 'js' ),
					$script_assets ['dependencies'],
					$js_file_version,
					true
				);

				if ( in_array( $script, $translated_scripts, true ) ) {
					wp_set_script_translations( $script, 'woocommerce' );
				}
			} catch ( \Exception $e ) {
				// Avoid crashing WordPress if an asset file could not be loaded.
				wc_caught_exception( $e, __CLASS__ . '::' . __FUNCTION__, $script_path_name );
			}
		}

		wp_register_style(
			'wc-components',
			self::get_url( 'components/style', 'css' ),
			array(),
			$css_file_version
		);
		wp_style_add_data( 'wc-components', 'rtl', 'replace' );

		wp_register_style(
			'wc-customer-effort-score',
			self::get_url( 'customer-effort-score/style', 'css' ),
			array(),
			$css_file_version
		);
		wp_style_add_data( 'wc-customer-effort-score', 'rtl', 'replace' );

		wp_register_style(
			'wc-experimental',
			self::get_url( 'experimental/style', 'css' ),
			array(),
			$css_file_version
		);
		wp_style_add_data( 'wc-experimental', 'rtl', 'replace' );

		wp_localize_script(
			WC_ADMIN_APP,
			'wcAdminAssets',
			array(
				'path'    => plugins_url( self::get_path( 'js' ), WC_ADMIN_PLUGIN_FILE ),
				'version' => $js_file_version,
			)
		);

		wp_register_style(
			WC_ADMIN_APP,
			self::get_url( 'app/style', 'css' ),
			array( 'wc-components', 'wc-customer-effort-score', 'wp-components', 'wc-experimental' ),
			$css_file_version
		);
		wp_style_add_data( WC_ADMIN_APP, 'rtl', 'replace' );

		wp_register_style(
			'wc-onboarding',
			self::get_url( 'onboarding/style', 'css' ),
			array(),
			$css_file_version
		);
		wp_style_add_data( 'wc-onboarding', 'rtl', 'replace' );
	}

	/**
	 * Injects wp-shared-settings as a dependency if it's present.
	 */
	public function inject_wc_settings_dependencies() {
		if ( wp_script_is( 'wc-settings', 'registered' ) ) {
			$handles_for_injection = [
				'wc-csv',
				'wc-currency',
				'wc-customer-effort-score',
				'wc-navigation',
				// NOTE: This should be removed when Gutenberg is updated and
				// the notices package is removed from WooCommerce Admin.
				'wc-notices',
				'wc-number',
				'wc-date',
				'wc-components',
				'wc-tracks',
			];
			foreach ( $handles_for_injection as $handle ) {
				$script = wp_scripts()->query( $handle, 'registered' );
				if ( $script instanceof _WP_Dependency ) {
					$script->deps[] = 'wc-settings';
				}
			}
		}
	}

	/**
	 * Loads a script
	 *
	 * @param string $script_path_name The script path name.
	 * @param string $script_name Filename of the script to load.
	 * @param bool   $need_translation Whether the script need translations.
	 */
	public static function register_script( $script_path_name, $script_name, $need_translation = false ) {
		$script_assets_filename = self::get_script_asset_filename( $script_path_name, $script_name );
		$script_assets          = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . $script_path_name . '/' . $script_assets_filename;

		wp_enqueue_script(
			'wc-admin-' . $script_name,
			self::get_url( $script_path_name . '/' . $script_name, 'js' ),
			array_merge( array( WC_ADMIN_APP ), $script_assets ['dependencies'] ),
			self::get_file_version( 'js' ),
			true
		);
		if ( $need_translation ) {
			wp_set_script_translations( 'wc-admin-' . $script_name, 'woocommerce' );
		}
	}
}
Internal/Admin/WCAdminUser.php000064400000007414152333011040012200 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Admin;

/**
 * WCAdminUser Class.
 */
class WCAdminUser {

	/**
	 * Class instance.
	 *
	 * @var WCAdminUser instance
	 */
	protected static $instance = null;

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'rest_api_init', array( $this, 'register_user_data' ) );
	}

	/**
	 * Get class instance.
	 *
	 * @return object Instance.
	 */
	public static function get_instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Registers WooCommerce specific user data to the WordPress user API.
	 */
	public function register_user_data() {
		register_rest_field(
			'user',
			'is_super_admin',
			array(
				'get_callback' => function() {
					return is_super_admin();
				},
				'schema'       => null,
			)
		);
		register_rest_field(
			'user',
			'woocommerce_meta',
			array(
				'get_callback'    => array( $this, 'get_user_data_values' ),
				'update_callback' => array( $this, 'update_user_data_values' ),
				'schema'          => null,
			)
		);
	}

	/**
	 * For all the registered user data fields (  Loader::get_user_data_fields ), fetch the data
	 * for returning via the REST API.
	 *
	 * @param WP_User $user Current user.
	 */
	public function get_user_data_values( $user ) {
		$values = array();
		foreach ( $this->get_user_data_fields() as $field ) {
			$values[ $field ] = self::get_user_data_field( $user['id'], $field );
		}
		return $values;
	}

	/**
	 * For all the registered user data fields ( Loader::get_user_data_fields ), update the data
	 * for the REST API.
	 *
	 * @param array   $values   The new values for the meta.
	 * @param WP_User $user     The current user.
	 * @param string  $field_id The field id for the user meta.
	 */
	public function update_user_data_values( $values, $user, $field_id ) {
		if ( empty( $values ) || ! is_array( $values ) || 'woocommerce_meta' !== $field_id ) {
			return;
		}
		$fields  = $this->get_user_data_fields();
		$updates = array();
		foreach ( $values as $field => $value ) {
			if ( in_array( $field, $fields, true ) ) {
				$updates[ $field ] = $value;
				self::update_user_data_field( $user->ID, $field, $value );
			}
		}
		return $updates;
	}

	/**
	 * We store some WooCommerce specific user meta attached to users endpoint,
	 * so that we can track certain preferences or values such as the inbox activity panel last open time.
	 * Additional fields can be added in the function below, and then used via wc-admin's currentUser data.
	 *
	 * @return array Fields to expose over the WP user endpoint.
	 */
	public function get_user_data_fields() {
		return apply_filters( 'woocommerce_admin_get_user_data_fields', array() );
	}

	/**
	 * Helper to update user data fields.
	 *
	 * @param int    $user_id  User ID.
	 * @param string $field Field name.
	 * @param mixed  $value  Field value.
	 */
	public static function update_user_data_field( $user_id, $field, $value ) {
		update_user_meta( $user_id, 'woocommerce_admin_' . $field, $value );
	}

	/**
	 * Helper to retrive user data fields.
	 *
	 * Migrates old key prefixes as well.
	 *
	 * @param int    $user_id  User ID.
	 * @param string $field Field name.
	 * @return mixed The user field value.
	 */
	public static function get_user_data_field( $user_id, $field ) {
		$meta_value = get_user_meta( $user_id, 'woocommerce_admin_' . $field, true );

		// Migrate old meta values (prefix changed from `wc_admin_` to `woocommerce_admin_`).
		if ( '' === $meta_value ) {
			$old_meta_value = get_user_meta( $user_id, 'wc_admin_' . $field, true );

			if ( '' !== $old_meta_value ) {
				self::update_user_data_field( $user_id, $field, $old_meta_value );
				delete_user_meta( $user_id, 'wc_admin_' . $field );

				$meta_value = $old_meta_value;
			}
		}

		return $meta_value;
	}
}
Internal/Admin/Notes/CustomizeStoreWithBlocks.php000064400000004545152333011040016142 0ustar00<?php
/**
 * WooCommerce Admin: Customize your online store with WooCommerce blocks.
 *
 * Adds a note to customize the client online store with WooCommerce blocks.
 *
 * @package WooCommerce\Admin
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Customize_Store_With_Blocks.
 */
class CustomizeStoreWithBlocks {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-customize-store-with-blocks';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		$onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() );

		// Confirm that $onboarding_profile is set.
		if ( empty( $onboarding_profile ) ) {
			return;
		}

		// Make sure that the person who filled out the OBW was not setting up
		// the store for their customer/client.
		if (
			! isset( $onboarding_profile['setup_client'] ) ||
			$onboarding_profile['setup_client']
		) {
			return;
		}

		// We want to show the note after fourteen days.
		if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4', 14 * DAY_IN_SECONDS ) ) {
			return;
		}

		// Don't show if there aren't products.
		$query    = new \WC_Product_Query(
			array(
				'limit'  => 1,
				'return' => 'ids',
				'status' => array( 'publish' ),
			)
		);
		$products = $query->get_products();
		if ( 0 === count( $products ) ) {
			return;
		}

		$note = new Note();
		$note->set_title( __( 'Customize your online store with WooCommerce blocks', 'woocommerce' ) );
		$note->set_content( __( 'With our blocks, you can select and display products, categories, filters, and more virtually anywhere on your site — no need to use shortcodes or edit lines of code. Learn more about how to use each one of them.', 'woocommerce' ) );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_content_data( (object) array() );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'customize-store-with-blocks',
			__( 'Learn more', 'woocommerce' ),
			'https://woocommerce.com/posts/how-to-customize-your-online-store-with-woocommerce-blocks/?utm_source=inbox&utm_medium=product',
			Note::E_WC_ADMIN_NOTE_ACTIONED
		);
		return $note;
	}
}
Internal/Admin/Notes/OnboardingPayments.php000064400000003364152333011040014752 0ustar00<?php
/**
 * WooCommerce Admin: Payments reminder note.
 *
 * Adds a notes to complete the payment methods.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Onboarding_Payments.
 */
class OnboardingPayments {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-onboarding-payments-reminder';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		// We want to show the note after five days.
		if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4', 5 * DAY_IN_SECONDS ) ) {
			return;
		}

		// Check to see if any gateways have been added.
		$gateways         = WC()->payment_gateways->get_available_payment_gateways();
		$enabled_gateways = array_filter(
			$gateways,
			function( $gateway ) {
				return 'yes' === $gateway->enabled;
			}
		);
		if ( ! empty( $enabled_gateways ) ) {
			return;
		}

		$note = new Note();
		$note->set_title( __( 'Start accepting payments on your store!', 'woocommerce' ) );
		$note->set_content( __( 'Take payments with the provider that’s right for you - choose from 100+ payment gateways for WooCommerce.', 'woocommerce' ) );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_content_data( (object) array() );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'view-payment-gateways',
			__( 'Learn more', 'woocommerce' ),
			'https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_medium=product',
			Note::E_WC_ADMIN_NOTE_ACTIONED,
			true
		);
		return $note;
	}
}
Internal/Admin/Notes/SellingOnlineCourses.php000064400000004603152333011040015252 0ustar00<?php
/**
 * WooCommerce Admin: Selling Online Courses note
 *
 * Adds a note to encourage selling online courses.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;
use \Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;

/**
 * Selling_Online_Courses
 */
class SellingOnlineCourses {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-selling-online-courses';

	/**
	 * Attach hooks.
	 */
	public function __construct() {
		add_action(
			'update_option_' . OnboardingProfile::DATA_OPTION,
			array( $this, 'check_onboarding_profile' ),
			10,
			3
		);
	}

	/**
	 * Check to see if the profiler options match before possibly adding note.
	 *
	 * @param object $old_value The old option value.
	 * @param object $value     The new option value.
	 * @param string $option    The name of the option.
	 */
	public static function check_onboarding_profile( $old_value, $value, $option ) {
		// Skip adding if this store is in the education/learning industry.
		if ( ! isset( $value['industry'] ) ) {
			return;
		}
		$industry_slugs = array_column( $value['industry'], 'slug' );
		if ( ! in_array( 'education-and-learning', $industry_slugs, true ) ) {
			return;
		}

		self::possibly_add_note();
	}

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		$note = new Note();

		$note->set_title( __( 'Do you want to sell online courses?', 'woocommerce' ) );
		$note->set_content( __( 'Online courses are a great solution for any business that can teach a new skill. Since courses don’t require physical product development or shipping, they’re affordable, fast to create, and can generate passive income for years to come. In this article, we provide you more information about selling courses using WooCommerce.', 'woocommerce' ) );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'learn-more',
			__( 'Learn more', 'woocommerce' ),
			'https://woocommerce.com/posts/how-to-sell-online-courses-wordpress/?utm_source=inbox&utm_medium=product',
			Note::E_WC_ADMIN_NOTE_ACTIONED
		);

		return $note;
	}
}
Internal/Admin/Notes/WelcomeToWooCommerceForStoreUsers.php000064400000003027152333011040017707 0ustar00<?php
/**
 * WooCommerce Admin: Welcome to WooCommerce for store users.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Welcome to WooCommerce for store users.
 */
class WelcomeToWooCommerceForStoreUsers {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-welcome-to-woocommerce-for-store-users';

	/**
	 * Attach hooks.
	 */
	public function __construct() {
		add_action( 'init', array( $this, 'possibly_add_note' ) );
	}

	/**
	 * Get the note.
	 *
	 * @return Note|null
	 */
	public static function get_note() {
		// Only add if coming from Calypso.
		if ( ! isset( $_GET['from-calypso'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			return;
		}

		$note = new Note();
		$note->set_title( __( 'Welcome to your new store management experience', 'woocommerce' ) );
		$note->set_content( __( "We've designed your navigation and home screen to help you focus on the things that matter most in managing your online store.", 'woocommerce' ) );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'learn-more',
			__( 'Learn more', 'woocommerce' ),
			'https://wordpress.com/support/new-woocommerce-experience-on-wordpress-dot-com/"',
			Note::E_WC_ADMIN_NOTE_ACTIONED,
			true
		);

		return $note;
	}
}
Internal/Admin/Notes/PaymentsMoreInfoNeeded.php000064400000004364152333011040015514 0ustar00<?php
/**
 * WooCommerce Admin Payments More Info Needed Inbox Note Provider
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
use Automattic\WooCommerce\Admin\PluginsHelper;

defined( 'ABSPATH' ) || exit;

/**
 * PaymentsMoreInfoNeeded
 */
class PaymentsMoreInfoNeeded {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-payments-more-info-needed';

	/**
	 * Should this note exist?
	 */
	public static function is_applicable() {
		return self::should_display_note();
	}

	/**
	 * Returns true if we should display the note.
	 *
	 * @return bool
	 */
	public static function should_display_note() {
		// If user has installed WCPay, don't show this note.
		$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
		if ( in_array( 'woocommerce-payments', $installed_plugins, true ) ) {
			return false;
		}

		// User has dismissed the WCPay Welcome Page.
		if ( 'yes' !== get_option( 'wc_calypso_bridge_payments_dismissed', 'no' ) ) {
			return false;
		}

		// More than 30 days since viewing the welcome page.
		$exit_survey_timestamp = get_option( 'wc_pay_exit_survey_more_info_needed_timestamp', false );
		if ( ! $exit_survey_timestamp ||
			( time() - $exit_survey_timestamp < 30 * DAY_IN_SECONDS )
		) {
			return false;
		}

		return true;
	}

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		if ( ! self::should_display_note() ) {
			return;
		}
		$content = __( 'We recently asked you if you wanted more information about WooCommerce Payments. Run your business and manage your payments in one place with the solution built and supported by WooCommerce.', 'woocommerce' );

		$note = new Note();
		$note->set_title( __( 'Payments made simple with WooCommerce Payments', 'woocommerce' ) );
		$note->set_content( $content );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action( 'learn-more', __( 'Learn more here', 'woocommerce' ), 'https://woocommerce.com/payments/' );
		return $note;
	}
}
Internal/Admin/Notes/MigrateFromShopify.php000064400000004330152333011040014717 0ustar00<?php
/**
 * WooCommerce Admin: Migrate from Shopify to WooCommerce.
 *
 * Adds a note to ask the client if they want to migrate from Shopify to WooCommerce.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Migrate_From_Shopify.
 */
class MigrateFromShopify {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-migrate-from-shopify';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {

		// We want to show the note after two days.
		$two_days = 2 * DAY_IN_SECONDS;
		if ( ! self::is_wc_admin_active_in_date_range( 'week-1', $two_days ) ) {
			return;
		}

		$onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() );
		if (
			! isset( $onboarding_profile['setup_client'] ) ||
			! isset( $onboarding_profile['selling_venues'] ) ||
			! isset( $onboarding_profile['other_platform'] )
		) {
			return;
		}

		// Make sure the client is not setup.
		if ( $onboarding_profile['setup_client'] ) {
			return;
		}

		// We will show the notification when the client already is selling and is using Shopify.
		if (
			'other' !== $onboarding_profile['selling_venues'] ||
			'shopify' !== $onboarding_profile['other_platform']
		) {
			return;
		}

		$note = new Note();
		$note->set_title( __( 'Do you want to migrate from Shopify to WooCommerce?', 'woocommerce' ) );
		$note->set_content( __( 'Changing eCommerce platforms might seem like a big hurdle to overcome, but it is easier than you might think to move your products, customers, and orders to WooCommerce. This article will help you with going through this process.', 'woocommerce' ) );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_content_data( (object) array() );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'migrate-from-shopify',
			__( 'Learn more', 'woocommerce' ),
			'https://woocommerce.com/posts/migrate-from-shopify-to-woocommerce/?utm_source=inbox&utm_medium=product',
			Note::E_WC_ADMIN_NOTE_ACTIONED
		);
		return $note;
	}
}
Internal/Admin/Notes/MarketingJetpack.php000064400000007265152333011040014376 0ustar00<?php
/**
 * WooCommerce Admin Jetpack Marketing Note Provider.
 *
 * Adds notes to the merchant's inbox concerning Jetpack Backup.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\Notes;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
use Automattic\WooCommerce\Admin\PluginsHelper;

/**
 * Suggest Jetpack Backup to Woo users.
 *
 * Note: This should probably live in the Jetpack plugin in the future.
 *
 * @see  https://developer.woocommerce.com/2020/10/16/using-the-admin-notes-inbox-in-woocommerce/
 */
class MarketingJetpack {
	// Shared Note Traits.
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-marketing-jetpack-backup';

	/**
	 * Product IDs that include Backup.
	 */
	const BACKUP_IDS = [
		2010,
		2011,
		2012,
		2013,
		2014,
		2015,
		2100,
		2101,
		2102,
		2103,
		2005,
		2006,
		2000,
		2003,
		2001,
		2004,
	];

	/**
	 * Maybe add a note on Jetpack Backups for Jetpack sites older than a week without Backups.
	 */
	public static function possibly_add_note() {
		/**
		 * Check if Jetpack is installed.
		 */
		$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
		if ( ! in_array( 'jetpack', $installed_plugins, true ) ) {
			return;
		}

		$data_store = \WC_Data_Store::load( 'admin-note' );

		// Do we already have this note?
		$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
		if ( ! empty( $note_ids ) ) {

			$note_id = array_pop( $note_ids );
			$note    = Notes::get_note( $note_id );
			if ( false === $note ) {
				return;
			}

			// If Jetpack Backups was purchased after the note was created, mark this note as actioned.
			if ( self::has_backups() && Note::E_WC_ADMIN_NOTE_ACTIONED !== $note->get_status() ) {
				$note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED );
				$note->save();
			}

			return;
		}

		// Check requirements.
		if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4', DAY_IN_SECONDS * 3 ) || ! self::can_be_added() || self::has_backups() ) {
			return;
		}

		// Add note.
		$note = self::get_note();
		$note->save();
	}

	/**
	 * Get the note.
	 */
	public static function get_note() {
		$note = new Note();
		$note->set_title( __( 'Protect your WooCommerce Store with Jetpack Backup.', 'woocommerce' ) );
		$note->set_content( __( 'Store downtime means lost sales. One-click restores get you back online quickly if something goes wrong.', 'woocommerce' ) );
		$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
		$note->set_name( self::NOTE_NAME );
		$note->set_layout( 'thumbnail' );
		$note->set_image(
			WC_ADMIN_IMAGES_FOLDER_URL . '/admin_notes/marketing-jetpack-2x.png'
		);
		$note->set_content_data( (object) array() );
		$note->set_source( 'woocommerce-admin-notes' );
		$note->add_action(
			'jetpack-backup-woocommerce',
			__( 'Get backups', 'woocommerce' ),
			esc_url( 'https://jetpack.com/upgrade/backup-woocommerce/?utm_source=inbox&utm_medium=automattic_referred&utm_campaign=jp_backup_to_woo' ),
			Note::E_WC_ADMIN_NOTE_ACTIONED
		);
		return $note;
	}

	/**
	 * Check if this blog already has a Jetpack Backups product.
	 *
	 * @return boolean  Whether or not this blog has backups.
	 */
	protected static function has_backups() {
		$product_ids = [];

		$plan = get_option( 'jetpack_active_plan' );
		if ( ! empty( $plan ) ) {
			$product_ids[] = $plan['product_id'];
		}

		$products = get_option( 'jetpack_site_products' );
		if ( ! empty( $products ) ) {
			foreach ( $products as $product ) {
				$product_ids[] = $product['product_id'];
			}
		}

		return (bool) array_intersect( self::BACKUP_IDS, $product_ids );
	}

}
Internal/Admin/Notes/AddFirstProduct.php000064400000005447152333011040014214 0ustar00<?php
/**
 * WooCommerce Admin: Add First Product.
 *
 * Adds a note (type `email`) to bring the client back to the store setup flow.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Add_First_Product.
 */
class AddFirstProduct {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-add-first-product-note';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		if ( ! self::wc_admin_active_for( 2 * DAY_IN_SECONDS ) || self::wc_admin_active_for( 5 * DAY_IN_SECONDS ) ) {
			return;
		}

		// Don't show if there is a product.
		$query    = new \WC_Product_Query(
			array(
				'limit'  => 1,
				'return' => 'ids',
				'status' => array( 'publish' ),
			)
		);
		$products = $query->get_products();
		if ( 0 !== count( $products ) ) {
			return;
		}

		// Don't show if there is an orders.
		$args   = array(
			'limit'  => 1,
			'return' => 'ids',
		);
		$orders = wc_get_orders( $args );
		if ( 0 !== count( $orders ) ) {
			return;
		}

		// If you're updating the following please use sprintf to separate HTML tags.
		// https://github.com/woocommerce/woocommerce-admin/pull/6617#discussion_r596889685.
		$content_lines = array(
			'{greetings}<br/><br/>',
			/* translators: %s: line break */
			sprintf( __( 'Nice one; you\'ve created a WooCommerce store! Now it\'s time to add your first product and get ready to start selling.%s', 'woocommerce' ), '<br/><br/>' ),
			__( 'There are three ways to add your products: you can <strong>create products manually, import them at once via CSV file</strong>, or <strong>migrate them from another service</strong>.<br/><br/>', 'woocommerce' ),
			/* translators: %1$s is an open anchor tag (<a>) and %2$s is a close link tag (</a>). */
			sprintf( __( '%1$1sExplore our docs%2$2s for more information, or just get started!', 'woocommerce' ), '<a href="https://woocommerce.com/document/managing-products/?utm_source=help_panel&utm_medium=product">', '</a>' ),
		);

		$additional_data = array(
			'role' => 'administrator',
		);

		$note = new Note();
		$note->set_title( __( 'Add your first product', 'woocommerce' ) );
		$note->set_content( implode( '', $content_lines ) );
		$note->set_content_data( (object) $additional_data );
		$note->set_image(
			plugins_url(
				'/images/admin_notes/dashboard-widget-setup.png',
				WC_ADMIN_PLUGIN_FILE
			)
		);
		$note->set_type( Note::E_WC_ADMIN_NOTE_EMAIL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action( 'add-first-product', __( 'Add a product', 'woocommerce' ), admin_url( 'admin.php?page=wc-admin&task=products' ) );
		return $note;
	}
}
Internal/Admin/Notes/TestCheckout.php000064400000005331152333011040013550 0ustar00<?php
/**
 * WooCommerce Admin Test Checkout.
 *
 * Adds a note to remind the user to test their store checkout.
 *
 * @package WooCommerce\Admin
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Test_Checkout
 */
class TestCheckout {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-test-checkout';

	/**
	 * Completed tasks option name.
	 */
	const TASK_LIST_TRACKED_TASKS = 'woocommerce_task_list_tracked_completed_tasks';

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'update_option_' . self::TASK_LIST_TRACKED_TASKS, array( $this, 'possibly_add_note' ) );
	}

	/**
	 * Get the note.
	 *
	 * @return Note|null
	 */
	public static function get_note() {
		$onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() );

		// Confirm that $onboarding_profile is set.
		if ( empty( $onboarding_profile ) ) {
			return;
		}

		// Make sure that the person who filled out the OBW was not setting up
		// the store for their customer/client.
		if (
			! isset( $onboarding_profile['setup_client'] ) ||
			$onboarding_profile['setup_client']
		) {
			return;
		}

		// Make sure payments task was completed.
		$completed_tasks = get_option( self::TASK_LIST_TRACKED_TASKS, array() );
		if ( ! in_array( 'payments', $completed_tasks, true ) ) {
			return;
		}

		// Make sure that products were added within the previous 1/2 hour.
		$query = new \WC_Product_Query(
			array(
				'limit'   => 1,
				'status'  => 'publish',
				'orderby' => 'date',
				'order'   => 'ASC',
			)
		);

		$products = $query->get_products();
		if ( 0 === count( $products ) ) {
			return;
		}

		$oldest_product_timestamp = $products[0]->get_date_created()->getTimestamp();
		$half_hour_in_seconds     = 30 * MINUTE_IN_SECONDS;
		if ( ( time() - $oldest_product_timestamp ) > $half_hour_in_seconds ) {
			return;
		}

		$content = __( 'Make sure that your checkout is working properly before you launch your store. Go through your checkout process in its entirety: from adding a product to your cart, choosing a shipping location, and making a payment.', 'woocommerce' );

		$note = new Note();
		$note->set_title( __( 'Don\'t forget to test your checkout', 'woocommerce' ) );
		$note->set_content( $content );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action( 'test-checkout', __( 'Test checkout', 'woocommerce' ), wc_get_page_permalink( 'shop' ) );
		return $note;
	}
}
Internal/Admin/Notes/TrackingOptIn.php000064400000005407152333011040013663 0ustar00<?php
/**
 * WooCommerce Admin Usage Tracking Opt In Note Provider.
 *
 * Adds a Usage Tracking Opt In extension note.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Tracking_Opt_In
 */
class TrackingOptIn {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-usage-tracking-opt-in';

	/**
	 * Attach hooks.
	 */
	public function __construct() {
		add_action( 'woocommerce_note_action_tracking-opt-in', array( $this, 'opt_in_to_tracking' ) );
	}

	/**
	 * Get the note.
	 *
	 * @return Note|null
	 */
	public static function get_note() {
		// Only show this note to stores that are opted out.
		if ( 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) {
			return;
		}

		// We want to show the note after one week.
		if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4' ) ) {
			return;
		}

		/* translators: 1: open link to WooCommerce.com settings, 2: open link to WooCommerce.com tracking documentation, 3: close link tag. */
		$content_format = __(
			'Gathering usage data allows us to improve WooCommerce. Your store will be considered as we evaluate new features, judge the quality of an update, or determine if an improvement makes sense. You can always visit the %1$sSettings%3$s and choose to stop sharing data. %2$sRead more%3$s about what data we collect.',
			'woocommerce'
		);

		$note_content = sprintf(
			$content_format,
			'<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced&section=woocommerce_com' ) ) . '" target="_blank">',
			'<a href="https://woocommerce.com/usage-tracking?utm_medium=product" target="_blank">',
			'</a>'
		);

		$note = new Note();
		$note->set_title( __( 'Help WooCommerce improve with usage tracking', 'woocommerce' ) );
		$note->set_content( $note_content );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action( 'tracking-opt-in', __( 'Activate usage tracking', 'woocommerce' ), false, Note::E_WC_ADMIN_NOTE_ACTIONED, true );
		return $note;
	}

	/**
	 * Opt in to usage tracking when note is actioned.
	 *
	 * @param Note $note Note being acted upon.
	 */
	public function opt_in_to_tracking( $note ) {
		if ( self::NOTE_NAME === $note->get_name() ) {
			// Opt in to tracking and schedule the first data update.
			// Same mechanism as in WC_Admin_Setup_Wizard::wc_setup_store_setup_save().
			update_option( 'woocommerce_allow_tracking', 'yes' );
			wp_schedule_single_event( time() + 10, 'woocommerce_tracker_send_event', array( true ) );
		}
	}
}
Internal/Admin/Notes/MerchantEmailNotifications.php000064400000006601152333011040016407 0ustar00<?php
/**
 * Handles merchant email notifications
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\Notes;

defined( 'ABSPATH' ) || exit;

/**
 * Merchant email notifications.
 * This gets all non-sent notes type `email` and sends them.
 */
class MerchantEmailNotifications {
	/**
	 * Initialize the merchant email notifications.
	 */
	public static function init() {
		add_action( 'admin_init', array( __CLASS__, 'trigger_notification_action' ) );
	}

	/**
	 * Trigger the note action.
	 */
	public static function trigger_notification_action() {
		/* phpcs:disable WordPress.Security.NonceVerification */
		if (
			! isset( $_GET['external_redirect'] ) ||
			1 !== intval( $_GET['external_redirect'] ) ||
			! isset( $_GET['user'] ) ||
			! isset( $_GET['note'] ) ||
			! isset( $_GET['action'] )
		) {
			return;
		}
		$note_id   = intval( $_GET['note'] );
		$action_id = intval( $_GET['action'] );
		$user_id   = intval( $_GET['user'] );
		/* phpcs:enable */

		$note = Notes::get_note( $note_id );

		if ( ! $note || Note::E_WC_ADMIN_NOTE_EMAIL !== $note->get_type() ) {
			return;
		}

		$triggered_action = Notes::get_action_by_id( $note, $action_id );

		if ( ! $triggered_action ) {
			return;
		}

		Notes::trigger_note_action( $note, $triggered_action );
		$url = $triggered_action->query;

		// We will use "wp_safe_redirect" when it's an internal redirect.
		if ( strpos( $url, 'http' ) === false ) {
			wp_safe_redirect( $url );
		} else {
			header( 'Location: ' . $url );
		}
		exit();
	}

	/**
	 * Send all the notifications type `email`.
	 */
	public static function run() {
		$data_store = Notes::load_data_store();
		$notes      = $data_store->get_notes(
			array(
				'type'   => array( Note::E_WC_ADMIN_NOTE_EMAIL ),
				'status' => array( 'unactioned' ),
			)
		);

		foreach ( $notes as $note ) {
			$note = Notes::get_note( $note->note_id );
			if ( $note ) {
				self::send_merchant_notification( $note );
				$note->set_status( 'sent' );
				$note->save();
			}
		}
	}

	/**
	 * Send the notification to the merchant.
	 *
	 * @param object $note The note to send.
	 */
	public static function send_merchant_notification( $note ) {
		\WC_Emails::instance();
		$users = self::get_notification_recipients( $note );
		$email = new EmailNotification( $note );
		foreach ( $users as $user ) {
			if ( is_email( $user->user_email ) ) {
				$name = self::get_merchant_preferred_name( $user );
				$email->trigger( $user->user_email, $user->ID, $name );
			}
		}
	}

	/**
	 * Get the preferred name for user. First choice is
	 * the user's first name, and then display_name.
	 *
	 * @param WP_User $user Recipient to send the note to.
	 * @return string User's name.
	 */
	public static function get_merchant_preferred_name( $user ) {
		$first_name = get_user_meta( $user->ID, 'first_name', true );
		if ( $first_name ) {
			return $first_name;
		}
		if ( $user->display_name ) {
			return $user->display_name;
		}
		return '';
	}

	/**
	 * Get users by role to notify.
	 *
	 * @param object $note The note to send.
	 * @return array Users to notify
	 */
	public static function get_notification_recipients( $note ) {
		$content_data = $note->get_content_data();
		$role         = 'administrator';
		if ( isset( $content_data->role ) ) {
			$role = $content_data->role;
		}
		$args = array( 'role' => $role );
		return get_users( $args );
	}
}
Internal/Admin/Notes/FirstProduct.php000064400000004177152333011040013602 0ustar00<?php
/**
 * WooCommerce Admin: Do you need help with adding your first product?
 *
 * Adds a note to ask the client if they need help adding their first product.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * First_Product.
 */
class FirstProduct {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-first-product';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		// We want to show the note after seven days.
		if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4' ) ) {
			return;
		}

		$onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() );

		// Confirm that $onboarding_profile is set.
		if ( empty( $onboarding_profile ) ) {
			return;
		}

		// Make sure that the person who filled out the OBW was not setting up
		// the store for their customer/client.
		if (
			! isset( $onboarding_profile['setup_client'] ) ||
			$onboarding_profile['setup_client']
		) {
			return;
		}

		// Don't show if there are products.
		$query    = new \WC_Product_Query(
			array(
				'limit'    => 1,
				'paginate' => true,
				'return'   => 'ids',
				'status'   => array( 'publish' ),
			)
		);
		$products = $query->get_products();
		$count    = $products->total;
		if ( 0 !== $count ) {
			return;
		}

		$note = new Note();
		$note->set_title( __( 'Do you need help with adding your first product?', 'woocommerce' ) );
		$note->set_content( __( 'This video tutorial will help you go through the process of adding your first product in WooCommerce.', 'woocommerce' ) );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_content_data( (object) array() );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'first-product-watch-tutorial',
			__( 'Watch tutorial', 'woocommerce' ),
			'https://www.youtube.com/watch?v=sFtXa00Jf_o&list=PLHdG8zvZd0E575Ia8Mu3w1h750YLXNfsC&index=24'
		);

		return $note;
	}
}
Internal/Admin/Notes/CustomizingProductCatalog.php000064400000004217152333011040016314 0ustar00<?php
/**
 * WooCommerce Admin: How to customize your product catalog note provider
 *
 * Adds a note with a link to the customizer a day after adding the first product
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Class CustomizingProductCatalog
 *
 * @package Automattic\WooCommerce\Admin\Notes
 */
class CustomizingProductCatalog {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-customizing-product-catalog';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		$query = new \WC_Product_Query(
			array(
				'limit'    => 1,
				'paginate' => true,
				'status'   => array( 'publish' ),
				'orderby'  => 'post_date',
				'order'    => 'DESC',
			)
		);

		$products = $query->get_products();

		// we need at least 1 product.
		if ( 0 === $products->total ) {
			return;
		}

		$product           = $products->products[0];
		$created_timestamp = $product->get_date_created()->getTimestamp();
		$is_a_day_old      = ( time() - $created_timestamp ) >= DAY_IN_SECONDS;

		// the product must be at least 1 day old.
		if ( ! $is_a_day_old ) {
			return;
		}

		// store must not been active more than 14 days.
		if ( self::wc_admin_active_for( DAY_IN_SECONDS * 14 ) ) {
			return;
		}

		$note = new Note();
		$note->set_title( __( 'How to customize your product catalog', 'woocommerce' ) );
		$note->set_content( __( 'You want your product catalog and images to look great and align with your brand. This guide will give you all the tips you need to get your products looking great in your store.', 'woocommerce' ) );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_content_data( (object) array() );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'day-after-first-product',
			__( 'Learn more', 'woocommerce' ),
			'https://woocommerce.com/document/woocommerce-customizer/?utm_source=inbox&utm_medium=product'
		);

		return $note;
	}
}
Internal/Admin/Notes/OrderMilestones.php000064400000022230152333011040014256 0ustar00<?php
/**
 * WooCommerce Admin (Dashboard) Order Milestones Note Provider.
 *
 * Adds a note to the merchant's inbox when certain order milestones are reached.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\Notes;
/**
 * Order_Milestones
 */
class OrderMilestones {
	/**
	 * Name of the "other milestones" note.
	 */
	const NOTE_NAME = 'wc-admin-orders-milestone';

	/**
	 * Option key name to store last order milestone.
	 */
	const LAST_ORDER_MILESTONE_OPTION_KEY = 'woocommerce_admin_last_orders_milestone';

	/**
	 * Hook to process order milestones.
	 */
	const PROCESS_ORDERS_MILESTONE_HOOK = 'wc_admin_process_orders_milestone';

	/**
	 * Allowed order statuses for calculating milestones.
	 *
	 * @var array
	 */
	protected $allowed_statuses = array(
		'pending',
		'processing',
		'completed',
	);

	/**
	 * Orders count cache.
	 *
	 * @var int
	 */
	protected $orders_count = null;

	/**
	 * Further order milestone thresholds.
	 *
	 * @var array
	 */
	protected $milestones = array(
		1,
		10,
		100,
		250,
		500,
		1000,
		5000,
		10000,
		500000,
		1000000,
	);

	/**
	 * Delay hook attachment until after the WC post types have been registered.
	 *
	 * This is required for retrieving the order count.
	 */
	public function __construct() {
		/**
		 * Filter Order statuses that will count towards milestones.
		 *
		 * @since 3.5.0
		 *
		 * @param array $allowed_statuses Order statuses that will count towards milestones.
		 */
		$this->allowed_statuses = apply_filters( 'woocommerce_admin_order_milestone_statuses', $this->allowed_statuses );

		add_action( 'woocommerce_after_register_post_type', array( $this, 'init' ) );
		register_deactivation_hook( WC_PLUGIN_FILE, array( $this, 'clear_scheduled_event' ) );
	}

	/**
	 * Hook everything up.
	 */
	public function init() {
		if ( ! wp_next_scheduled( self::PROCESS_ORDERS_MILESTONE_HOOK ) ) {
			wp_schedule_event( time(), 'hourly', self::PROCESS_ORDERS_MILESTONE_HOOK );
		}

		add_action( 'wc_admin_installed', array( $this, 'backfill_last_milestone' ) );

		add_action( self::PROCESS_ORDERS_MILESTONE_HOOK, array( $this, 'possibly_add_note' ) );
	}

	/**
	 * Clear out our hourly milestone hook upon plugin deactivation.
	 */
	public function clear_scheduled_event() {
		wp_clear_scheduled_hook( self::PROCESS_ORDERS_MILESTONE_HOOK );
	}

	/**
	 * Get the total count of orders (in the allowed statuses).
	 *
	 * @param bool $no_cache Optional. Skip cache.
	 * @return int Total orders count.
	 */
	public function get_orders_count( $no_cache = false ) {
		if ( $no_cache || is_null( $this->orders_count ) ) {
			$status_counts      = array_map( 'wc_orders_count', $this->allowed_statuses );
			$this->orders_count = array_sum( $status_counts );
		}

		return $this->orders_count;
	}

	/**
	 * Backfill the store's current milestone.
	 *
	 * Used to avoid celebrating milestones that were reached before plugin activation.
	 */
	public function backfill_last_milestone() {
		// If the milestone notes have been disabled via filter, bail.
		if ( ! $this->are_milestones_enabled() ) {
			return;
		}

		$this->set_last_milestone( $this->get_current_milestone() );
	}

	/**
	 * Get the store's last milestone.
	 *
	 * @return int Last milestone reached.
	 */
	public function get_last_milestone() {
		return get_option( self::LAST_ORDER_MILESTONE_OPTION_KEY, 0 );
	}

	/**
	 * Update the last reached milestone.
	 *
	 * @param int $milestone Last milestone reached.
	 */
	public function set_last_milestone( $milestone ) {
		update_option( self::LAST_ORDER_MILESTONE_OPTION_KEY, $milestone );
	}

	/**
	 * Calculate the current orders milestone.
	 *
	 * Based on the threshold values in $this->milestones.
	 *
	 * @return int Current orders milestone.
	 */
	public function get_current_milestone() {
		$milestone_reached = 0;
		$orders_count      = $this->get_orders_count();

		foreach ( $this->milestones as $milestone ) {
			if ( $milestone <= $orders_count ) {
				$milestone_reached = $milestone;
			}
		}

		return $milestone_reached;
	}

	/**
	 * Get the appropriate note title for a given milestone.
	 *
	 * @param int $milestone Order milestone.
	 * @return string Note title for the milestone.
	 */
	public static function get_note_title_for_milestone( $milestone ) {
		switch ( $milestone ) {
			case 1:
				return __( 'First order received', 'woocommerce' );
			case 10:
			case 100:
			case 250:
			case 500:
			case 1000:
			case 5000:
			case 10000:
			case 500000:
			case 1000000:
				return sprintf(
					/* translators: Number of orders processed. */
					__( 'Congratulations on processing %s orders!', 'woocommerce' ),
					wc_format_decimal( $milestone )
				);
			default:
				return '';
		}
	}

	/**
	 * Get the appropriate note content for a given milestone.
	 *
	 * @param int $milestone Order milestone.
	 * @return string Note content for the milestone.
	 */
	public static function get_note_content_for_milestone( $milestone ) {
		switch ( $milestone ) {
			case 1:
				return __( 'Congratulations on getting your first order! Now is a great time to learn how to manage your orders.', 'woocommerce' );
			case 10:
				return __( "You've hit the 10 orders milestone! Look at you go. Browse some WooCommerce success stories for inspiration.", 'woocommerce' );
			case 100:
			case 250:
			case 500:
			case 1000:
			case 5000:
			case 10000:
			case 500000:
			case 1000000:
				return __( 'Another order milestone! Take a look at your Orders Report to review your orders to date.', 'woocommerce' );
			default:
				return '';
		}
	}

	/**
	 * Get the appropriate note action for a given milestone.
	 *
	 * @param int $milestone Order milestone.
	 * @return array Note actoion (name, label, query) for the milestone.
	 */
	public static function get_note_action_for_milestone( $milestone ) {
		switch ( $milestone ) {
			case 1:
				return array(
					'name'  => 'learn-more',
					'label' => __( 'Learn more', 'woocommerce' ),
					'query' => 'https://woocommerce.com/document/managing-orders/?utm_source=inbox&utm_medium=product',
				);
			case 10:
				return array(
					'name'  => 'browse',
					'label' => __( 'Browse', 'woocommerce' ),
					'query' => 'https://woocommerce.com/success-stories/?utm_source=inbox&utm_medium=product',
				);
			case 100:
			case 250:
			case 500:
			case 1000:
			case 5000:
			case 10000:
			case 500000:
			case 1000000:
				return array(
					'name'  => 'review-orders',
					'label' => __( 'Review your orders', 'woocommerce' ),
					'query' => '?page=wc-admin&path=/analytics/orders',
				);
			default:
				return array(
					'name'  => '',
					'label' => '',
					'query' => '',
				);
		}
	}

	/**
	 * Convenience method to see if the milestone notes are enabled.
	 *
	 * @return boolean True if milestone notifications are enabled.
	 */
	public function are_milestones_enabled() {
		/**
		 * Filter to allow for disabling order milestones.
		 *
		 * @since 3.7.0
		 *
		 * @param boolean default true
		 */
		$milestone_notes_enabled = apply_filters( 'woocommerce_admin_order_milestones_enabled', true );

		return $milestone_notes_enabled;
	}

	/**
	 * Get the note. This is used for localizing the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		$note = Notes::get_note_by_name( self::NOTE_NAME );
		if ( ! $note ) {
			return false;
		}
		$content_data = $note->get_content_data();
		if ( ! isset( $content_data->current_milestone ) ) {
			return false;
		}
		return self::get_note_by_milestone(
			$content_data->current_milestone
		);
	}

	/**
	 * Get the note by milestones.
	 *
	 * @param int $current_milestone Current milestone.
	 *
	 * @return Note
	 */
	public static function get_note_by_milestone( $current_milestone ) {
		$content_data = (object) array(
			'current_milestone' => $current_milestone,
		);

		$note = new Note();
		$note->set_title( self::get_note_title_for_milestone( $current_milestone ) );
		$note->set_content( self::get_note_content_for_milestone( $current_milestone ) );
		$note->set_content_data( $content_data );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note_action = self::get_note_action_for_milestone( $current_milestone );
		$note->add_action( $note_action['name'], $note_action['label'], $note_action['query'] );
		return $note;
	}

	/**
	 * Checks if a note can and should be added.
	 *
	 * @return bool
	 */
	public function can_be_added() {
		// If the milestone notes have been disabled via filter, bail.
		if ( ! $this->are_milestones_enabled() ) {
			return false;
		}

		$last_milestone    = $this->get_last_milestone();
		$current_milestone = $this->get_current_milestone();

		if ( $current_milestone <= $last_milestone ) {
			return false;
		}

		return true;
	}

	/**
	 * Add milestone notes for other significant thresholds.
	 */
	public function possibly_add_note() {
		if ( ! self::can_be_added() ) {
			return;
		}
		$current_milestone = $this->get_current_milestone();
		$this->set_last_milestone( $current_milestone );

		// We only want one milestone note at any time.
		Notes::delete_notes_with_name( self::NOTE_NAME );
		$note = $this->get_note_by_milestone( $current_milestone );
		$note->save();
	}
}
Internal/Admin/Notes/LaunchChecklist.php000064400000003270152333011040014207 0ustar00<?php
/**
 * WooCommerce Admin Launch Checklist Note.
 *
 * Adds a note to cover pre-launch checklist items for store owners.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Launch_Checklist
 */
class LaunchChecklist {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-launch-checklist';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		// Only add this note if completing the task list or completed 3 tasks in 10 days.
		$completed_tasks     = get_option( 'woocommerce_task_list_tracked_completed_tasks', array() );
		$ten_days_in_seconds = 10 * DAY_IN_SECONDS;
		if (
			! get_option( 'woocommerce_task_list_complete' ) &&
			(
				count( $completed_tasks ) < 3 ||
				self::is_wc_admin_active_in_date_range( 'week-1-4', $ten_days_in_seconds )
			)
		) {
			return;
		}

		$content = __( 'To make sure you never get that sinking "what did I forget" feeling, we\'ve put together the essential pre-launch checklist.', 'woocommerce' );

		$note = new Note();
		$note->set_title( __( 'Ready to launch your store?', 'woocommerce' ) );
		$note->set_content( $content );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woocommerce.com/posts/pre-launch-checklist-the-essentials/?utm_source=inbox&utm_medium=product' );
		return $note;
	}
}
Internal/Admin/Notes/RealTimeOrderAlerts.php000064400000003014152333011040015010 0ustar00<?php
/**
 * WooCommerce Admin Real Time Order Alerts Note.
 *
 * Adds a note to download the mobile app to monitor store activity.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Real_Time_Order_Alerts
 */
class RealTimeOrderAlerts {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-real-time-order-alerts';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		// Only add this note if the store is 3 months old.
		if ( ! self::is_wc_admin_active_in_date_range( 'month-3-6' ) ) {
			return;
		}

		// Check that the previous mobile app note was not actioned.
		if ( MobileApp::has_note_been_actioned() ) {
			return;
		}

		$content = __( 'Get notifications about store activity, including new orders and product reviews directly on your mobile devices with the Woo app.', 'woocommerce' );

		$note = new Note();
		$note->set_title( __( 'Get real-time order alerts anywhere', 'woocommerce' ) );
		$note->set_content( $content );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woocommerce.com/mobile/?utm_source=inbox&utm_medium=product' );
		return $note;
	}
}
Internal/Admin/Notes/CouponPageMoved.php000064400000007471152333011040014205 0ustar00<?php
/**
 * WooCommerce Admin Coupon Page Moved provider.
 *
 * Adds a notice when the store manager access the coupons page via the old WooCommerce > Coupons menu.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\Notes;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
use Automattic\WooCommerce\Internal\Admin\CouponsMovedTrait;
use stdClass;
use WC_Data_Store;

/**
 * Coupon_Page_Moved class.
 */
class CouponPageMoved {

	use NoteTraits, CouponsMovedTrait;

	const NOTE_NAME = 'wc-admin-coupon-page-moved';

	/**
	 * Initialize our hooks.
	 */
	public function init() {
		if ( ! wc_coupons_enabled() ) {
			return;
		}

		add_action( 'admin_init', [ $this, 'possibly_add_note' ] );
		add_action( 'admin_init', [ $this, 'redirect_to_coupons' ] );
		add_action( 'woocommerce_admin_newly_installed', [ $this, 'disable_legacy_menu_for_new_install' ] );
	}

	/**
	 * Checks if a note can and should be added.
	 *
	 * @return bool
	 */
	public static function can_be_added() {
		if ( ! wc_coupons_enabled() ) {
			return false;
		}

		// Don't add the notice if the legacy coupon menu is already disabled.
		if ( ! self::should_display_legacy_menu() ) {
			return false;
		}

		// Don't add the notice if it's been hidden by the user before.
		if ( self::has_dismissed_note() ) {
			return false;
		}

		// If we already have a notice, don't add a new one.
		if ( self::has_unactioned_note() ) {
			return false;
		}

		return isset( $_GET[ self::$query_key ] ) && (bool) $_GET[ self::$query_key ]; // phpcs:ignore WordPress.Security.NonceVerification
	}

	/**
	 * Get the note object for this class.
	 *
	 * @return Note
	 */
	public static function get_note() {
		$note = new Note();
		$note->set_title( __( 'Coupon management has moved!', 'woocommerce' ) );
		$note->set_content( __( 'Coupons can now be managed from Marketing > Coupons. Click the button below to remove the legacy WooCommerce > Coupons menu item.', 'woocommerce' ) );
		$note->set_type( Note::E_WC_ADMIN_NOTE_UPDATE );
		$note->set_name( self::NOTE_NAME );
		$note->set_content_data( new stdClass() );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'remove-legacy-coupon-menu',
			__( 'Remove legacy coupon menu', 'woocommerce' ),
			wc_admin_url( '&action=remove-coupon-menu' ),
			Note::E_WC_ADMIN_NOTE_ACTIONED
		);

		return $note;
	}

	/**
	 * Find notes that have not been actioned.
	 *
	 * @return bool
	 */
	protected static function has_unactioned_note() {
		$note = Notes::get_note_by_name( self::NOTE_NAME );

		if ( ! $note ) {
			return false;
		}

		return $note->get_status() === 'unactioned';
	}

	/**
	 * Whether any notes have been dismissed by the user previously.
	 *
	 * @return bool
	 */
	protected static function has_dismissed_note() {
		$note = Notes::get_note_by_name( self::NOTE_NAME );

		if ( ! $note ) {
			return false;
		}

		return ! $note->get_is_deleted();
	}

	/**
	 * Get the data store object.
	 *
	 * @return DataStore The data store object.
	 */
	protected static function get_data_store() {
		return WC_Data_Store::load( 'admin-note' );
	}

	/**
	 * Safe redirect to the coupon page to force page refresh.
	 */
	public function redirect_to_coupons() {
		/* phpcs:disable WordPress.Security.NonceVerification */
		if (
			! isset( $_GET['page'] ) ||
			'wc-admin' !== $_GET['page'] ||
			! isset( $_GET['action'] ) ||
			'remove-coupon-menu' !== $_GET['action'] ||
			! defined( 'WC_ADMIN_PLUGIN_FILE' )
		) {
			return;
		}
		/* phpcs:enable */
		$this->display_legacy_menu( false );
		wp_safe_redirect( self::get_management_url( 'coupons' ) );
		exit;
	}

	/**
	 * Disable legacy coupon menu when installing for the first time.
	 */
	public function disable_legacy_menu_for_new_install() {
		$this->display_legacy_menu( false );
	}
}
Internal/Admin/Notes/InstallJPAndWCSPlugins.php000064400000011126152333011040015344 0ustar00<?php
/**
 * WooCommerce Admin Add Install Jetpack and WooCommerce Shipping & Tax Plugin Note Provider.
 *
 * Adds a note to the merchant's inbox prompting them to install the Jetpack
 * and WooCommerce Shipping & Tax plugins after it fails to install during
 * WooCommerce setup.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\Notes;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;
use \Automattic\WooCommerce\Admin\PluginsHelper;

/**
 * Install_JP_And_WCS_Plugins
 */
class InstallJPAndWCSPlugins {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-install-jp-and-wcs-plugins';

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'woocommerce_note_action_install-jp-and-wcs-plugins', array( $this, 'install_jp_and_wcs_plugins' ) );
		add_action( 'activated_plugin', array( $this, 'action_note' ) );
		add_action( 'woocommerce_plugins_install_api_error', array( $this, 'on_install_error' ) );
		add_action( 'woocommerce_plugins_install_error', array( $this, 'on_install_error' ) );
		add_action( 'woocommerce_plugins_activate_error', array( $this, 'on_install_error' ) );
	}

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		$content = __( 'We noticed that there was a problem during the Jetpack and WooCommerce Shipping & Tax install. Please try again and enjoy all the advantages of having the plugins connected to your store! Sorry for the inconvenience. The "Jetpack" and "WooCommerce Shipping & Tax" plugins will be installed & activated for free.', 'woocommerce' );

		$note = new Note();
		$note->set_title( __( 'Uh oh... There was a problem during the Jetpack and WooCommerce Shipping & Tax install. Please try again.', 'woocommerce' ) );
		$note->set_content( $content );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'install-jp-and-wcs-plugins',
			__( 'Install plugins', 'woocommerce' ),
			false,
			Note::E_WC_ADMIN_NOTE_ACTIONED
		);
		return $note;
	}

	/**
	 * Action the Install Jetpack and WooCommerce Shipping & Tax note, if any exists,
	 * and as long as both the Jetpack and WooCommerce Shipping & Tax plugins have been
	 * activated.
	 */
	public static function action_note() {
		// Make sure that both plugins are active before actioning the note.
		$active_plugin_slugs = PluginsHelper::get_active_plugin_slugs();
		$jp_active           = in_array( 'jetpack', $active_plugin_slugs, true );
		$wcs_active          = in_array( 'woocommerce-services', $active_plugin_slugs, true );

		if ( ! $jp_active || ! $wcs_active ) {
			return;
		}

		// Action any notes with a matching name.
		$data_store = Notes::load_data_store();
		$note_ids   = $data_store->get_notes_with_name( self::NOTE_NAME );

		foreach ( $note_ids as $note_id ) {
			$note = Notes::get_note( $note_id );

			if ( $note ) {
				$note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED );
				$note->save();
			}
		}
	}

	/**
	 * Install the Jetpack and WooCommerce Shipping & Tax plugins in response to the action
	 * being clicked in the admin note.
	 *
	 * @param Note $note The note being actioned.
	 */
	public function install_jp_and_wcs_plugins( $note ) {
		if ( self::NOTE_NAME !== $note->get_name() ) {
			return;
		}

		$this->install_and_activate_plugin( 'jetpack' );
		$this->install_and_activate_plugin( 'woocommerce-services' );
	}

	/**
	 * Installs and activates the specified plugin.
	 *
	 * @param string $plugin The plugin slug.
	 */
	private function install_and_activate_plugin( $plugin ) {
		$install_request = array( 'plugin' => $plugin );
		$installer       = new \Automattic\WooCommerce\Admin\API\OnboardingPlugins();
		$result          = $installer->install_plugin( $install_request );

		// @todo Use the error statuses to decide whether or not to action the note.
		if ( is_wp_error( $result ) ) {
			return;
		}

		$activate_request = array( 'plugins' => $plugin );

		$installer->activate_plugins( $activate_request );
	}

	/**
	 * Create an alert notification in response to an error installing a plugin.
	 *
	 * @param string $slug The slug of the plugin being installed.
	 */
	public function on_install_error( $slug ) {
		// Exit early if we're not installing the Jetpack or the WooCommerce Shipping & Tax plugins.
		if ( 'jetpack' !== $slug && 'woocommerce-services' !== $slug ) {
			return;
		}

		self::possibly_add_note();
	}
}
Internal/Admin/Notes/PerformanceOnMobile.php000064400000003217152333011040015032 0ustar00<?php
/**
 * WooCommerce Admin Performance on mobile note.
 *
 * Adds a note to download the mobile app, performance on mobile.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Performance_On_Mobile
 */
class PerformanceOnMobile {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-performance-on-mobile';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		// Only add this note if this store is at least 9 months old.
		$nine_months_in_seconds = MONTH_IN_SECONDS * 9;
		if ( ! self::wc_admin_active_for( $nine_months_in_seconds ) ) {
			return;
		}

		// Check that the previous mobile app notes have not been actioned.
		if ( MobileApp::has_note_been_actioned() ) {
			return;
		}
		if ( RealTimeOrderAlerts::has_note_been_actioned() ) {
			return;
		}
		if ( ManageOrdersOnTheGo::has_note_been_actioned() ) {
			return;
		}

		$note = new Note();

		$note->set_title( __( 'Track your store performance on mobile', 'woocommerce' ) );
		$note->set_content( __( 'Monitor your sales and high performing products with the Woo app.', 'woocommerce' ) );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'learn-more',
			__( 'Learn more', 'woocommerce' ),
			'https://woocommerce.com/mobile/?utm_source=inbox&utm_medium=product'
		);

		return $note;
	}
}
Internal/Admin/Notes/EditProductsOnTheMove.php000064400000003266152333011040015346 0ustar00<?php
/**
 * WooCommerce Admin Edit products on the move note.
 *
 * Adds a note to download the mobile app.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Edit_Products_On_The_Move
 */
class EditProductsOnTheMove {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-edit-products-on-the-move';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		// Only add this note if this store is at least a year old.
		$year_in_seconds = 365 * DAY_IN_SECONDS;
		if ( ! self::wc_admin_active_for( $year_in_seconds ) ) {
			return;
		}

		// Check that the previous mobile app notes have not been actioned.
		if ( MobileApp::has_note_been_actioned() ) {
			return;
		}
		if ( RealTimeOrderAlerts::has_note_been_actioned() ) {
			return;
		}
		if ( ManageOrdersOnTheGo::has_note_been_actioned() ) {
			return;
		}
		if ( PerformanceOnMobile::has_note_been_actioned() ) {
			return;
		}

		$note = new Note();

		$note->set_title( __( 'Edit products on the move', 'woocommerce' ) );
		$note->set_content( __( 'Edit and create new products from your mobile devices with the Woo app', 'woocommerce' ) );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'learn-more',
			__( 'Learn more', 'woocommerce' ),
			'https://woocommerce.com/mobile/?utm_source=inbox&utm_medium=product'
		);

		return $note;
	}
}
Internal/Admin/Notes/GivingFeedbackNotes.php000064400000003002152333011040014775 0ustar00<?php
/**
 * WooCommerce Admin (Dashboard) Giving feedback notes provider
 *
 * Adds notes to the merchant's inbox about giving feedback.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
use Automattic\WooCommerce\Internal\Admin\Survey;

/**
 * Giving_Feedback_Notes
 */
class GivingFeedbackNotes {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-store-notice-giving-feedback-2';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4' ) ) {
			return;
		}

		// Otherwise, create our new note.
		$note = new Note();
		$note->set_title( __( 'You\'re invited to share your experience', 'woocommerce' ) );
		$note->set_content( __( 'Now that you’ve chosen us as a partner, our goal is to make sure we\'re providing the right tools to meet your needs. We\'re looking forward to having your feedback on the store setup experience so we can improve it in the future.', 'woocommerce' ) );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'share-feedback',
			__( 'Share feedback', 'woocommerce' ),
			Survey::get_url( '/store-setup-survey' )
		);
		return $note;
	}
}
Internal/Admin/Notes/ChoosingTheme.php000064400000002710152333011040013675 0ustar00<?php
/**
 * WooCommerce Admin (Dashboard) choosing a theme note
 *
 * Adds notes to the merchant's inbox about choosing a theme.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Giving_Feedback_Notes
 */
class ChoosingTheme {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-choosing-a-theme';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		// We need to show choosing a theme notification after 1 day of install.
		if ( ! self::is_wc_admin_active_in_date_range( 'week-1', DAY_IN_SECONDS ) ) {
			return;
		}

		// Otherwise, create our new note.
		$note = new Note();
		$note->set_title( __( 'Choosing a theme?', 'woocommerce' ) );
		$note->set_content( __( 'Check out the themes that are compatible with WooCommerce and choose one aligned with your brand and business needs.', 'woocommerce' ) );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'visit-the-theme-marketplace',
			__( 'Visit the theme marketplace', 'woocommerce' ),
			'https://woocommerce.com/product-category/themes/?utm_source=inbox&utm_medium=product'
		);
		return $note;
	}
}
Internal/Admin/Notes/ManageOrdersOnTheGo.php000064400000003053152333011040014735 0ustar00<?php
/**
 * WooCommerce Admin Manage orders on the go note.
 *
 * Adds a note to download the mobile app to manage orders on the go.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Manage_Orders_On_The_Go
 */
class ManageOrdersOnTheGo {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-manage-orders-on-the-go';

	/**
	 * Get the note.
	 *
	 * @return Note|null
	 */
	public static function get_note() {
		// Only add this note if this store is at least 6 months old.
		if ( ! self::is_wc_admin_active_in_date_range( 'month-6+' ) ) {
			return;
		}

		// Check that the previous mobile app notes have not been actioned.
		if ( MobileApp::has_note_been_actioned() ) {
			return;
		}
		if ( RealTimeOrderAlerts::has_note_been_actioned() ) {
			return;
		}

		$note = new Note();

		$note->set_title( __( 'Manage your orders on the go', 'woocommerce' ) );
		$note->set_content( __( 'Look for orders, customer info, and process refunds in one click with the Woo app.', 'woocommerce' ) );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'learn-more',
			__( 'Learn more', 'woocommerce' ),
			'https://woocommerce.com/mobile/?utm_source=inbox&utm_medium=product'
		);

		return $note;
	}
}
Internal/Admin/Notes/WooSubscriptionsNotes.php000064400000033601152333011040015511 0ustar00<?php
/**
 * WooCommerce Admin (Dashboard) WooCommerce.com Extension Subscriptions Note Provider.
 *
 * Adds notes to the merchant's inbox concerning WooCommerce.com extension subscriptions.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\Notes;

/**
 * Woo_Subscriptions_Notes
 */
class WooSubscriptionsNotes {
	const LAST_REFRESH_OPTION_KEY = 'woocommerce_admin-wc-helper-last-refresh';
	const NOTE_NAME               = 'wc-admin-wc-helper-connection';
	const CONNECTION_NOTE_NAME    = 'wc-admin-wc-helper-connection';
	const SUBSCRIPTION_NOTE_NAME  = 'wc-admin-wc-helper-subscription';
	const NOTIFY_WHEN_DAYS_LEFT   = 60;

	/**
	 * We want to bubble up expiration notices when they cross certain age
	 * thresholds. PHP 5.2 doesn't support constant arrays, so we do this.
	 *
	 * @return array
	 */
	private function get_bump_thresholds() {
		return array( 60, 45, 20, 7, 1 ); // days.
	}

	/**
	 * Hook all the things.
	 */
	public function __construct() {
		add_action( 'admin_init', array( $this, 'admin_init' ) );
		add_action( 'update_option_woocommerce_helper_data', array( $this, 'update_option_woocommerce_helper_data' ), 10, 2 );
	}

	/**
	 * Reacts to changes in the helper option.
	 *
	 * @param array $old_value The previous value of the option.
	 * @param array $value The new value of the option.
	 */
	public function update_option_woocommerce_helper_data( $old_value, $value ) {
		if ( ! is_array( $old_value ) ) {
			$old_value = array();
		}
		if ( ! is_array( $value ) ) {
			$value = array();
		}

		$old_auth  = array_key_exists( 'auth', $old_value ) ? $old_value['auth'] : array();
		$new_auth  = array_key_exists( 'auth', $value ) ? $value['auth'] : array();
		$old_token = array_key_exists( 'access_token', $old_auth ) ? $old_auth['access_token'] : '';
		$new_token = array_key_exists( 'access_token', $new_auth ) ? $new_auth['access_token'] : '';

		// The site just disconnected.
		if ( ! empty( $old_token ) && empty( $new_token ) ) {
			$this->remove_notes();
			$this->add_no_connection_note();
			return;
		}

		// The site is connected.
		if ( $this->is_connected() ) {
			$this->remove_notes();
			$this->refresh_subscription_notes();
			return;
		}
	}

	/**
	 * Things to do on admin_init.
	 */
	public function admin_init() {
		$this->check_connection();

		if ( $this->is_connected() ) {
			$refresh_notes = false;

			// Did the user just do something on the helper page?.
			if ( isset( $_GET['wc-helper-status'] ) ) { // @codingStandardsIgnoreLine.
				$refresh_notes = true;
			}

			// Has it been more than a day since we last checked?
			// Note: We do it this way and not wp_scheduled_task since WC_Helper_Options is not loaded for cron.
			$time_now_gmt = current_time( 'timestamp', 0 );
			$last_refresh = intval( get_option( self::LAST_REFRESH_OPTION_KEY, 0 ) );
			if ( $last_refresh + DAY_IN_SECONDS <= $time_now_gmt ) {
				update_option( self::LAST_REFRESH_OPTION_KEY, $time_now_gmt );
				$refresh_notes = true;
			}

			if ( $refresh_notes ) {
				$this->refresh_subscription_notes();
			}
		}
	}

	/**
	 * Checks the connection. Adds a note (as necessary) if there is no connection.
	 */
	public function check_connection() {
		if ( ! $this->is_connected() ) {
			$data_store = Notes::load_data_store();
			$note_ids   = $data_store->get_notes_with_name( self::CONNECTION_NOTE_NAME );
			if ( ! empty( $note_ids ) ) {
				// We already have a connection note. Exit early.
				return;
			}

			$this->remove_notes();
			$this->add_no_connection_note();
		}
	}

	/**
	 * Whether or not we think the site is currently connected to WooCommerce.com.
	 *
	 * @return bool
	 */
	public function is_connected() {
		$auth = \WC_Helper_Options::get( 'auth' );
		return ( ! empty( $auth['access_token'] ) );
	}

	/**
	 * Returns the WooCommerce.com provided site ID for this site.
	 *
	 * @return int|false
	 */
	public function get_connected_site_id() {
		if ( ! $this->is_connected() ) {
			return false;
		}

		$auth = \WC_Helper_Options::get( 'auth' );
		return absint( $auth['site_id'] );
	}

	/**
	 * Returns an array of product_ids whose subscriptions are active on this site.
	 *
	 * @return array
	 */
	public function get_subscription_active_product_ids() {
		$site_id = $this->get_connected_site_id();
		if ( ! $site_id ) {
			return array();
		}

		$product_ids = array();

		if ( $this->is_connected() ) {
			$subscriptions = \WC_Helper::get_subscriptions();

			foreach ( (array) $subscriptions as $subscription ) {
				if ( in_array( $site_id, $subscription['connections'], true ) ) {
					$product_ids[] = $subscription['product_id'];
				}
			}
		}

		return $product_ids;
	}

	/**
	 * Clears all connection or subscription notes.
	 */
	public function remove_notes() {
		Notes::delete_notes_with_name( self::CONNECTION_NOTE_NAME );
		Notes::delete_notes_with_name( self::SUBSCRIPTION_NOTE_NAME );
	}

	/**
	 * Adds a note prompting to connect to WooCommerce.com.
	 */
	public function add_no_connection_note() {
		$note = self::get_note();
		$note->save();
	}

	/**
	 * Get the WooCommerce.com connection note
	 */
	public static function get_note() {
		$note = new Note();
		$note->set_title( __( 'Connect to WooCommerce.com', 'woocommerce' ) );
		$note->set_content( __( 'Connect to get important product notifications and updates.', 'woocommerce' ) );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::CONNECTION_NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'connect',
			__( 'Connect', 'woocommerce' ),
			'?page=wc-addons&section=helper',
			Note::E_WC_ADMIN_NOTE_UNACTIONED
		);
		return $note;
	}

	/**
	 * Gets the product_id (if any) associated with a note.
	 *
	 * @param Note $note The note object to interrogate.
	 * @return int|false
	 */
	public function get_product_id_from_subscription_note( &$note ) {
		$content_data = $note->get_content_data();

		if ( property_exists( $content_data, 'product_id' ) ) {
			return intval( $content_data->product_id );
		}

		return false;
	}

	/**
	 * Removes notes for product_ids no longer active on this site.
	 */
	public function prune_inactive_subscription_notes() {
		$active_product_ids = $this->get_subscription_active_product_ids();

		$data_store = Notes::load_data_store();
		$note_ids   = $data_store->get_notes_with_name( self::SUBSCRIPTION_NOTE_NAME );

		foreach ( (array) $note_ids as $note_id ) {
			$note       = Notes::get_note( $note_id );
			$product_id = $this->get_product_id_from_subscription_note( $note );
			if ( ! empty( $product_id ) ) {
				if ( ! in_array( $product_id, $active_product_ids, true ) ) {
					$note->delete();
				}
			}
		}
	}

	/**
	 * Finds a note for a given product ID, if the note exists at all.
	 *
	 * @param int $product_id The product ID to search for.
	 * @return Note|false
	 */
	public function find_note_for_product_id( $product_id ) {
		$product_id = intval( $product_id );

		$data_store = Notes::load_data_store();
		$note_ids   = $data_store->get_notes_with_name( self::SUBSCRIPTION_NOTE_NAME );
		foreach ( (array) $note_ids as $note_id ) {
			$note             = Notes::get_note( $note_id );
			$found_product_id = $this->get_product_id_from_subscription_note( $note );

			if ( $product_id === $found_product_id ) {
				return $note;
			}
		}

		return false;
	}

	/**
	 * Deletes a note for a given product ID, if the note exists at all.
	 *
	 * @param int $product_id The product ID to search for.
	 */
	public function delete_any_note_for_product_id( $product_id ) {
		$product_id = intval( $product_id );

		$note = $this->find_note_for_product_id( $product_id );
		if ( $note ) {
			$note->delete();
		}
	}

	/**
	 * Adds or updates a note for an expiring subscription.
	 *
	 * @param array $subscription The subscription to work with.
	 */
	public function add_or_update_subscription_expiring( $subscription ) {
		$product_id            = $subscription['product_id'];
		$product_name          = $subscription['product_name'];
		$expires               = intval( $subscription['expires'] );
		$time_now_gmt          = current_time( 'timestamp', 0 );
		$days_until_expiration = intval( ceil( ( $expires - $time_now_gmt ) / DAY_IN_SECONDS ) );

		$note = $this->find_note_for_product_id( $product_id );

		if ( $note ) {
			$content_data = $note->get_content_data();
			if ( property_exists( $content_data, 'days_until_expiration' ) ) {
				// Note: There is no reason this property should not exist. This is just defensive programming.
				$note_days_until_expiration = intval( $content_data->days_until_expiration );
				if ( $days_until_expiration === $note_days_until_expiration ) {
					// Note is already up to date. Bail.
					return;
				}

				// If we have a note and we are at or have crossed a threshold, we should delete
				// the old note and create a new one, thereby "bumping" the note to the top of the inbox.
				$bump_thresholds    = $this->get_bump_thresholds();
				$crossing_threshold = false;

				foreach ( (array) $bump_thresholds as $bump_threshold ) {
					if ( ( $note_days_until_expiration > $bump_threshold ) && ( $days_until_expiration <= $bump_threshold ) ) {
						$note->delete();
						$note = false;
						continue;
					}
				}
			}
		}

		$note_title = sprintf(
			/* translators: name of the extension subscription expiring soon */
			__( '%s subscription expiring soon', 'woocommerce' ),
			$product_name
		);

		$note_content = sprintf(
			/* translators: number of days until the subscription expires */
			__( 'Your subscription expires in %d days. Enable autorenew to avoid losing updates and access to support.', 'woocommerce' ),
			$days_until_expiration
		);

		$note_content_data = (object) array(
			'product_id'            => $product_id,
			'product_name'          => $product_name,
			'expired'               => false,
			'days_until_expiration' => $days_until_expiration,
		);

		if ( ! $note ) {
			$note = new Note();
		}

		// Reset everything in case we are repurposing an expired note as an expiring note.
		$note->set_title( $note_title );
		$note->set_type( Note::E_WC_ADMIN_NOTE_WARNING );
		$note->set_name( self::SUBSCRIPTION_NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->clear_actions();
		$note->add_action(
			'enable-autorenew',
			__( 'Enable Autorenew', 'woocommerce' ),
			'https://woocommerce.com/my-account/my-subscriptions/?utm_medium=product'
		);
		$note->set_content( $note_content );
		$note->set_content_data( $note_content_data );
		$note->save();
	}

	/**
	 * Adds a note for an expired subscription, or updates an expiring note to expired.
	 *
	 * @param array $subscription The subscription to work with.
	 */
	public function add_or_update_subscription_expired( $subscription ) {
		$product_id   = $subscription['product_id'];
		$product_name = $subscription['product_name'];
		$product_page = $subscription['product_url'];
		$expires      = intval( $subscription['expires'] );
		$expires_date = gmdate( 'F jS', $expires );

		$note = $this->find_note_for_product_id( $product_id );
		if ( $note ) {
			$note_content_data = $note->get_content_data();
			if ( $note_content_data->expired ) {
				// We've already got a full fledged expired note for this. Bail.
				// Expired notes' content don't change with time.
				return;
			}
		}

		$note_title = sprintf(
			/* translators: name of the extension subscription that expired */
			__( '%s subscription expired', 'woocommerce' ),
			$product_name
		);

		$note_content = sprintf(
			/* translators: date the subscription expired, e.g. Jun 7th 2018 */
			__( 'Your subscription expired on %s. Get a new subscription to continue receiving updates and access to support.', 'woocommerce' ),
			$expires_date
		);

		$note_content_data = (object) array(
			'product_id'   => $product_id,
			'product_name' => $product_name,
			'expired'      => true,
			'expires'      => $expires,
			'expires_date' => $expires_date,
		);

		if ( ! $note ) {
			$note = new Note();
		}

		$note->set_title( $note_title );
		$note->set_content( $note_content );
		$note->set_content_data( $note_content_data );
		$note->set_type( Note::E_WC_ADMIN_NOTE_WARNING );
		$note->set_name( self::SUBSCRIPTION_NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->clear_actions();
		$note->add_action(
			'renew-subscription',
			__( 'Renew Subscription', 'woocommerce' ),
			$product_page
		);
		$note->save();
	}

	/**
	 * For each active subscription on this site, checks the expiration date and creates/updates/deletes notes.
	 */
	public function refresh_subscription_notes() {
		if ( ! $this->is_connected() ) {
			return;
		}

		$this->prune_inactive_subscription_notes();

		$subscriptions      = \WC_Helper::get_subscriptions();
		$active_product_ids = $this->get_subscription_active_product_ids();

		foreach ( (array) $subscriptions as $subscription ) {
			// Only concern ourselves with active products.
			$product_id = $subscription['product_id'];
			if ( ! in_array( $product_id, $active_product_ids, true ) ) {
				continue;
			}

			// If the subscription will auto-renew, clean up and exit.
			if ( $subscription['autorenew'] ) {
				$this->delete_any_note_for_product_id( $product_id );
				continue;
			}

			// If the subscription is not expiring by the first threshold, clean up and exit.
			$bump_thresholds = $this->get_bump_thresholds();
			$first_threshold = DAY_IN_SECONDS * $bump_thresholds[0];
			$expires         = intval( $subscription['expires'] );
			$time_now_gmt    = current_time( 'timestamp', 0 );
			if ( $expires > $time_now_gmt + $first_threshold ) {
				$this->delete_any_note_for_product_id( $product_id );
				continue;
			}

			// Otherwise, if the subscription can still have auto-renew enabled, let them know that now.
			if ( $expires > $time_now_gmt ) {
				$this->add_or_update_subscription_expiring( $subscription );
				continue;
			}

			// If we got this far, the subscription has completely expired, let them know.
			$this->add_or_update_subscription_expired( $subscription );
		}
	}
}
Internal/Admin/Notes/EUVATNumber.php000064400000003214152333011040013176 0ustar00<?php
/**
 * WooCommerce Admin: EU VAT Number Note.
 *
 * Adds a note for EU store to install the EU VAT Number extension.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * EU_VAT_Number
 */
class EUVATNumber {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-eu-vat-number';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		if ( 'yes' !== get_option( 'wc_connect_taxes_enabled', 'no' ) ) {
			return;
		}

		$country_code = WC()->countries->get_base_country();
		$eu_countries = WC()->countries->get_european_union_countries();
		if ( ! in_array( $country_code, $eu_countries, true ) ) {
			return;
		}

		$content = __( "If your store is based in the EU, we recommend using the EU VAT Number extension in addition to automated taxes. It provides your checkout with a field to collect and validate a customer's EU VAT number, if they have one.", 'woocommerce' );

		$note = new Note();
		$note->set_title( __( 'Collect and validate EU VAT numbers at checkout', 'woocommerce' ) );
		$note->set_content( $content );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'learn-more',
			__( 'Learn more', 'woocommerce' ),
			'https://woocommerce.com/products/eu-vat-number/?utm_medium=product',
			Note::E_WC_ADMIN_NOTE_ACTIONED
		);
		return $note;
	}
}
Internal/Admin/Notes/WooCommercePayments.php000064400000014414152333011040015105 0ustar00<?php
/**
 * WooCommerce Admin WooCommerce Payments Note Provider.
 *
 * Adds a note to the merchant's inbox showing the benefits of the WooCommerce Payments.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\Notes;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * WooCommerce_Payments
 */
class WooCommercePayments {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-woocommerce-payments';

	/**
	 * Name of the note for use in the database.
	 */
	const PLUGIN_SLUG = 'woocommerce-payments';

	/**
	 * Name of the note for use in the database.
	 */
	const PLUGIN_FILE = 'woocommerce-payments/woocommerce-payments.php';

	/**
	 * Attach hooks.
	 */
	public function __construct() {
		add_action( 'init', array( $this, 'install_on_action' ) );
		add_action( 'wc-admin-woocommerce-payments_add_note', array( $this, 'add_note' ) );
	}

	/**
	 * Maybe add a note on WooCommerce Payments for US based sites older than a week without the plugin installed.
	 */
	public static function possibly_add_note() {
		if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4' ) || 'US' !== WC()->countries->get_base_country() ) {
			return;
		}

		$data_store = Notes::load_data_store();

		// We already have this note? Then mark the note as actioned.
		$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
		if ( ! empty( $note_ids ) ) {

			$note_id = array_pop( $note_ids );
			$note    = Notes::get_note( $note_id );
			if ( false === $note ) {
				return;
			}

			// If the WooCommerce Payments plugin was installed after the note was created, make sure it's marked as actioned.
			if ( self::is_installed() && Note::E_WC_ADMIN_NOTE_ACTIONED !== $note->get_status() ) {
				$note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED );
				$note->save();
			}

			return;
		}

		$current_date = new \DateTime();
		$publish_date = new \DateTime( '2020-04-14' );

		if ( $current_date >= $publish_date ) {

			$note = self::get_note();
			if ( self::can_be_added() ) {
				$note->save();
			}

			return;

		} else {

			$hook_name = sprintf( '%s_add_note', self::NOTE_NAME );

			if ( ! WC()->queue()->get_next( $hook_name ) ) {
				WC()->queue()->schedule_single( $publish_date->getTimestamp(), $hook_name );
			}
		}
	}

	/**
	 * Add a note about WooCommerce Payments.
	 *
	 * @return Note
	 */
	public static function get_note() {
		$note = new Note();
		$note->set_title( __( 'Try the new way to get paid', 'woocommerce' ) );
		$note->set_content(
			__( 'Securely accept credit and debit cards on your site. Manage transactions without leaving your WordPress dashboard. Only with <strong>WooCommerce Payments</strong>.', 'woocommerce' ) .
			'<br><br>' .
			sprintf(
				/* translators: 1: opening link tag, 2: closing tag */
				__( 'By clicking "Get started", you agree to our %1$sTerms of Service%2$s', 'woocommerce' ),
				'<a href="https://wordpress.com/tos/" target="_blank">',
				'</a>'
			)
		);
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woocommerce.com/payments/?utm_medium=product', Note::E_WC_ADMIN_NOTE_UNACTIONED );
		$note->add_action( 'get-started', __( 'Get started', 'woocommerce' ), wc_admin_url( '&action=setup-woocommerce-payments' ), Note::E_WC_ADMIN_NOTE_ACTIONED, true );
		$note->add_nonce_to_action( 'get-started', 'setup-woocommerce-payments', '' );

		// Create the note as "actioned" if the plugin is already installed.
		if ( self::is_installed() ) {
			$note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED );
		}
		return $note;
	}


	/**
	 * Check if the WooCommerce Payments plugin is active or installed.
	 */
	protected static function is_installed() {
		if ( defined( 'WC_Payments' ) ) {
			return true;
		}
		include_once ABSPATH . '/wp-admin/includes/plugin.php';
		return 0 === validate_plugin( self::PLUGIN_FILE );
	}

	/**
	 * Install and activate WooCommerce Payments.
	 *
	 * @return boolean Whether the plugin was successfully activated.
	 */
	private function install_and_activate_wcpay() {
		$install_request = array( 'plugins' => self::PLUGIN_SLUG );
		$installer       = new \Automattic\WooCommerce\Admin\API\Plugins();
		$result          = $installer->install_plugins( $install_request );
		if ( is_wp_error( $result ) ) {
			return false;
		}

		wc_admin_record_tracks_event( 'woocommerce_payments_install', array( 'context' => 'inbox' ) );

		$activate_request = array( 'plugins' => self::PLUGIN_SLUG );
		$result           = $installer->activate_plugins( $activate_request );
		if ( is_wp_error( $result ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Install & activate WooCommerce Payments plugin, and redirect to setup.
	 */
	public function install_on_action() {
		// TODO: Need to validate this request more strictly since we're taking install actions directly?
		if (
			! isset( $_GET['page'] ) ||
			'wc-admin' !== $_GET['page'] ||
			! isset( $_GET['action'] ) ||
			'setup-woocommerce-payments' !== $_GET['action']
		) {
			return;
		}

		$data_store = Notes::load_data_store();

		// We already have this note? Then mark the note as actioned.
		$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
		if ( empty( $note_ids ) ) {
			return;
		}

		$note_id = array_pop( $note_ids );
		$note    = Notes::get_note( $note_id );
		if ( false === $note ) {
			return;
		}
		$action = $note->get_action( 'get-started' );
		if ( ! $action ||
			( isset( $action->nonce_action ) &&
				(
					empty( $_GET['_wpnonce'] ) ||
					! wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), $action->nonce_action ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
				)
			)
		) {
			return;
		}

		if ( ! current_user_can( 'install_plugins' ) ) {
			return;
		}

		$this->install_and_activate_wcpay();

		// WooCommerce Payments is installed at this point, so link straight into the onboarding flow.
		$connect_url = add_query_arg(
			array(
				'wcpay-connect' => '1',
				'_wpnonce'      => wp_create_nonce( 'wcpay-connect' ),
			),
			admin_url()
		);
		wp_safe_redirect( $connect_url );
		exit;
	}
}
Internal/Admin/Notes/MagentoMigration.php000064400000004714152333011040014413 0ustar00<?php
/**
 * WooCommerce Admin note on how to migrate from Magento.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Features\Onboarding;
use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * MagentoMigration
 */
class MagentoMigration {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-magento-migration';

	/**
	 * Attach hooks.
	 */
	public function __construct() {
		add_action( 'update_option_' . OnboardingProfile::DATA_OPTION, array( __CLASS__, 'possibly_add_note' ) );
		add_action( 'woocommerce_admin_magento_migration_note', array( __CLASS__, 'save_note' ) );
	}

	/**
	 * Add the note if it passes predefined conditions.
	 */
	public static function possibly_add_note() {
		$onboarding_profile = get_option( OnboardingProfile::DATA_OPTION, array() );

		if ( empty( $onboarding_profile ) ) {
			return;
		}

		if (
			! isset( $onboarding_profile['other_platform'] ) ||
			'magento' !== $onboarding_profile['other_platform']
		) {
			return;
		}

		if (
			! isset( $onboarding_profile['setup_client'] ) ||
			$onboarding_profile['setup_client']
		) {
			return;
		}

		WC()->queue()->schedule_single( time() + ( 5 * MINUTE_IN_SECONDS ), 'woocommerce_admin_magento_migration_note' );
	}

	/**
	 * Save the note to the database.
	 */
	public static function save_note() {
		$note = self::get_note();

		if ( self::note_exists() ) {
			return;
		}

		$note->save();
	}

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		$note = new Note();

		$note->set_title( __( 'How to Migrate from Magento to WooCommerce', 'woocommerce' ) );
		$note->set_content( __( 'Changing platforms might seem like a big hurdle to overcome, but it is easier than you might think to move your products, customers, and orders to WooCommerce. This article will help you with going through this process.', 'woocommerce' ) );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'learn-more',
			__( 'Learn more', 'woocommerce' ),
			'https://woocommerce.com/posts/how-migrate-from-magento-to-woocommerce/?utm_source=inbox'
		);

		return $note;
	}
}
Internal/Admin/Notes/PaymentsRemindMeLater.php000064400000004222152333011040015352 0ustar00<?php
/**
 * WooCommerce Admin Payment Reminder Me later
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
use Automattic\WooCommerce\Admin\PluginsHelper;

defined( 'ABSPATH' ) || exit;

/**
 * PaymentsRemindMeLater
 */
class PaymentsRemindMeLater {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-payments-remind-me-later';

	/**
	 * Should this note exist?
	 */
	public static function is_applicable() {
		return self::should_display_note();
	}

	/**
	 * Returns true if we should display the note.
	 *
	 * @return bool
	 */
	public static function should_display_note() {
		// Installed WCPay.
		$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
		if ( in_array( 'woocommerce-payments', $installed_plugins, true ) ) {
			return false;
		}
		// Dismissed WCPay welcome page.
		if ( 'yes' === get_option( 'wc_calypso_bridge_payments_dismissed', 'no' ) ) {
			return false;
		}

		// Less than 3 days since viewing welcome page.
		$view_timestamp = get_option( 'wc_pay_welcome_page_viewed_timestamp', false );
		if ( ! $view_timestamp ||
			( time() - $view_timestamp < 3 * DAY_IN_SECONDS )
		) {
			return false;
		}
		return true;
	}


	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		if ( ! self::should_display_note() ) {
			return;
		}
		$content = __( 'Save up to $800 in fees by managing transactions with WooCommerce Payments. With WooCommerce Payments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies.', 'woocommerce' );

		$note = new Note();
		$note->set_title( __( 'Save big with WooCommerce Payments', 'woocommerce' ) );
		$note->set_content( $content );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), admin_url( 'admin.php?page=wc-admin&path=/wc-pay-welcome-page' ) );
		return $note;
	}
}
Internal/Admin/Notes/UnsecuredReportFiles.php000064400000004113152333011040015254 0ustar00<?php
/**
 * WooCommerce Admin Unsecured Files Note.
 *
 * Adds a warning about potentially unsecured files.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;

if ( ! class_exists( Note::class ) ) {
	class_alias( WC_Admin_Note::class, Note::class );
}

/**
 * Unsecured_Report_Files
 */
class UnsecuredReportFiles {

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-remove-unsecured-report-files';

	/**
	 * Get the note.
	 *
	 * @return Note|null
	 */
	public static function get_note() {
		$note = new Note();
		$note->set_title( __( 'Potentially unsecured files were found in your uploads directory', 'woocommerce' ) );
		$note->set_content(
			sprintf(
				/* translators: 1: opening analytics docs link tag. 2: closing link tag */
				__( 'Files that may contain %1$sstore analytics%2$s reports were found in your uploads directory - we recommend assessing and deleting any such files.', 'woocommerce' ),
				'<a href="https://woocommerce.com/document/woocommerce-analytics/" target="_blank">',
				'</a>'
			)
		);
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_ERROR );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'learn-more',
			__( 'Learn more', 'woocommerce' ),
			'https://developer.woocommerce.com/?p=10410',
			Note::E_WC_ADMIN_NOTE_UNACTIONED,
			true
		);
		$note->add_action(
			'dismiss',
			__( 'Dismiss', 'woocommerce' ),
			wc_admin_url(),
			Note::E_WC_ADMIN_NOTE_ACTIONED,
			false
		);

		return $note;
	}

	/**
	 * Add the note if it passes predefined conditions.
	 */
	public static function possibly_add_note() {
		$note = self::get_note();

		if ( self::note_exists() ) {
			return;
		}

		$note->save();
	}

	/**
	 * Check if the note has been previously added.
	 */
	public static function note_exists() {
		$data_store = \WC_Data_Store::load( 'admin-note' );
		$note_ids   = $data_store->get_notes_with_name( self::NOTE_NAME );
		return ! empty( $note_ids );
	}

}
Internal/Admin/Notes/WooCommerceSubscriptions.php000064400000003605152333011040016154 0ustar00<?php
/**
 * WooCommerce Admin: WooCommerce Subscriptions.
 *
 * Adds a note to learn more about WooCommerce Subscriptions.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;
use \Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;

/**
 * WooCommerce_Subscriptions.
 */
class WooCommerceSubscriptions {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-woocommerce-subscriptions';

	/**
	 * Get the note.
	 *
	 * @return Note|null
	 */
	public static function get_note() {
		$onboarding_data = get_option( OnboardingProfile::DATA_OPTION, array() );

		if ( ! isset( $onboarding_data['product_types'] ) || ! in_array( 'subscriptions', $onboarding_data['product_types'], true ) ) {
			return;
		}

		if ( ! self::is_wc_admin_active_in_date_range( 'week-1', DAY_IN_SECONDS ) ) {
			return;
		}

		$note = new Note();
		$note->set_title( __( 'Do you need more info about WooCommerce Subscriptions?', 'woocommerce' ) );
		$note->set_content( __( 'WooCommerce Subscriptions allows you to introduce a variety of subscriptions for physical or virtual products and services. Create product-of-the-month clubs, weekly service subscriptions or even yearly software billing packages. Add sign-up fees, offer free trials, or set expiration periods.', 'woocommerce' ) );
		$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
		$note->set_name( self::NOTE_NAME );
		$note->set_content_data( (object) array() );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'learn-more',
			__( 'Learn More', 'woocommerce' ),
			'https://woocommerce.com/products/woocommerce-subscriptions/?utm_source=inbox&utm_medium=product',
			Note::E_WC_ADMIN_NOTE_UNACTIONED,
			true
		);
		return $note;
	}
}
Internal/Admin/Notes/PersonalizeStore.php000064400000003650152333011040014455 0ustar00<?php
/**
 * WooCommerce Admin Personalize Your Store Note Provider.
 *
 * Adds a note to the merchant's inbox prompting them to personalize their store.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Notes\Note;
use \Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Personalize_Store
 */
class PersonalizeStore {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-personalize-store';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		// Only show the note to stores with homepage.
		$homepage_id = get_option( 'woocommerce_onboarding_homepage_post_id', false );
		if ( ! $homepage_id ) {
			return;
		}

		// Show the note after task list is done.
		$is_task_list_complete = get_option( 'woocommerce_task_list_complete', false );

		// We want to show the note after day 5.
		$five_days_in_seconds = 5 * DAY_IN_SECONDS;

		if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4', $five_days_in_seconds ) && ! $is_task_list_complete ) {
			return;
		}

		$content = __( 'The homepage is one of the most important entry points in your store. When done right it can lead to higher conversions and engagement. Don\'t forget to personalize the homepage that we created for your store during the onboarding.', 'woocommerce' );

		$note = new Note();
		$note->set_title( __( 'Personalize your store\'s homepage', 'woocommerce' ) );
		$note->set_content( $content );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action( 'personalize-homepage', __( 'Personalize homepage', 'woocommerce' ), admin_url( 'post.php?post=' . $homepage_id . '&action=edit' ), Note::E_WC_ADMIN_NOTE_ACTIONED );
		return $note;
	}
}
Internal/Admin/Notes/MobileApp.php000064400000002615152333011040013015 0ustar00<?php
/**
 * WooCommerce Admin Mobile App Note Provider.
 *
 * Adds a note to the merchant's inbox showing the benefits of the mobile app.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Mobile_App
 */
class MobileApp {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-mobile-app';

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		// We want to show the mobile app note after day 2.
		$two_days_in_seconds = 2 * DAY_IN_SECONDS;
		if ( ! self::is_wc_admin_active_in_date_range( 'week-1', $two_days_in_seconds ) ) {
			return;
		}

		$content = __( 'Install the WooCommerce mobile app to manage orders, receive sales notifications, and view key metrics — wherever you are.', 'woocommerce' );

		$note = new Note();
		$note->set_title( __( 'Install Woo mobile app', 'woocommerce' ) );
		$note->set_content( $content );
		$note->set_content_data( (object) array() );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woocommerce.com/mobile/?utm_medium=product' );
		return $note;
	}
}
Internal/Admin/Notes/EmailNotification.php000064400000012250152333011040014537 0ustar00<?php
/**
 * Handles emailing user notes.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Automattic\WooCommerce\Admin\Notes\Notes;

/**
 * Include dependencies.
 */
if ( ! class_exists( 'WC_Email', false ) ) {
	include_once WC_ABSPATH . 'includes/emails/class-wc-email.php';
}

/**
 * EmailNotification Class.
 */
class EmailNotification extends \WC_Email {
	/**
	 * Constructor.
	 *
	 * @param Note $note The notification to send.
	 */
	public function __construct( $note ) {
		$this->note          = $note;
		$this->id            = 'merchant_notification';
		$this->template_base = WC_ADMIN_ABSPATH . 'includes/react-admin/emails/';
		$this->placeholders  = array(
			'{greetings}' => __( 'Hi there,', 'woocommerce' ),
		);

		// Call parent constructor.
		parent::__construct();
	}

	/**
	 * This email has no user-facing settings.
	 */
	public function init_form_fields() {}

	/**
	 * This email has no user-facing settings.
	 */
	public function init_settings() {}

	/**
	 * Return template filename.
	 *
	 * @param string $type Type of email to send.
	 * @return string
	 */
	public function get_template_filename( $type = 'html' ) {
		if ( ! in_array( $type, array( 'html', 'plain' ), true ) ) {
			return;
		}
		$content_data      = $this->note->get_content_data();
		$template_filename = "{$type}-merchant-notification.php";
		if ( isset( $content_data->{"template_{$type}"} ) && file_exists( $this->template_base . $content_data->{ "template_{$type}" } ) ) {
			$template_filename = $content_data[ "template_{$type}" ];
		}
		return $template_filename;
	}

	/**
	 * Return email type.
	 *
	 * @return string
	 */
	public function get_email_type() {
		return class_exists( 'DOMDocument' ) ? 'html' : 'plain';
	}

	/**
	 * Get email heading.
	 *
	 * @return string
	 */
	public function get_default_heading() {
		$content_data = $this->note->get_content_data();
		if ( isset( $content_data->heading ) ) {
			return $content_data->heading;
		}

		return $this->note->get_title();
	}

	/**
	 * Get email headers.
	 *
	 * @return string
	 */
	public function get_headers() {
		$header = 'Content-Type: ' . $this->get_content_type() . "\r\n";
		return apply_filters( 'woocommerce_email_headers', $header, $this->id, $this->object, $this );
	}

	/**
	 * Get email subject.
	 *
	 * @return string
	 */
	public function get_default_subject() {
		return $this->note->get_title();
	}

	/**
	 * Get note content.
	 *
	 * @return string
	 */
	public function get_note_content() {
		return $this->note->get_content();
	}

	/**
	 * Get note image.
	 *
	 * @return string
	 */
	public function get_image() {
		return $this->note->get_image();
	}

	/**
	 * Get email action.
	 *
	 * @return stdClass
	 */
	public function get_actions() {
		return $this->note->get_actions();
	}

	/**
	 * Get content html.
	 *
	 * @return string
	 */
	public function get_content_html() {
		return wc_get_template_html(
			$this->get_template_filename( 'html' ),
			array(
				'email_actions'           => $this->get_actions(),
				'email_content'           => $this->format_string( $this->get_note_content() ),
				'email_heading'           => $this->format_string( $this->get_heading() ),
				'email_image'             => $this->get_image(),
				'sent_to_admin'           => true,
				'plain_text'              => false,
				'email'                   => $this,
				'opened_tracking_url'     => $this->opened_tracking_url,
				'trigger_note_action_url' => $this->trigger_note_action_url,
			),
			'',
			$this->template_base
		);
	}

	/**
	 * Get content plain.
	 *
	 * @return string
	 */
	public function get_content_plain() {
		return wc_get_template_html(
			$this->get_template_filename( 'plain' ),
			array(
				'email_heading'           => $this->format_string( $this->get_heading() ),
				'email_content'           => $this->format_string( $this->get_note_content() ),
				'email_actions'           => $this->get_actions(),
				'sent_to_admin'           => true,
				'plain_text'              => true,
				'email'                   => $this,
				'trigger_note_action_url' => $this->trigger_note_action_url,
			),
			'',
			$this->template_base
		);
	}

	/**
	 * Trigger the sending of this email.
	 *
	 * @param string $user_email Email to send the note.
	 * @param int    $user_id User id to to track the note.
	 * @param string $user_name User's name.
	 */
	public function trigger( $user_email, $user_id, $user_name ) {
		$this->recipient               = $user_email;
		$this->opened_tracking_url     = sprintf(
			'%1$s/wp-json/wc-analytics/admin/notes/tracker/%2$d/user/%3$d',
			site_url(),
			$this->note->get_id(),
			$user_id
		);
		$this->trigger_note_action_url = sprintf(
			'%1$s&external_redirect=1&note=%2$d&user=%3$d&action=',
			wc_admin_url(),
			$this->note->get_id(),
			$user_id
		);

		if ( $user_name ) {
			/* translators: %s = merchant name */
			$this->placeholders['{greetings}'] = sprintf( __( 'Hi %s,', 'woocommerce' ), $user_name );
		}

		$this->send(
			$this->get_recipient(),
			$this->get_subject(),
			$this->get_content(),
			$this->get_headers(),
			$this->get_attachments()
		);
		Notes::record_tracks_event_with_user( $user_id, 'email_note_sent', array( 'note_name' => $this->note->get_name() ) );
	}
}
Internal/Admin/Notes/NewSalesRecord.php000064400000012404152333011040014022 0ustar00<?php
/**
 * WooCommerce Admin (Dashboard) New Sales Record Note Provider.
 *
 * Adds a note to the merchant's inbox when the previous day's sales are a new record.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\Notes;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * New_Sales_Record
 */
class NewSalesRecord {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-new-sales-record';

	/**
	 * Option name for the sales record date in ISO 8601 (YYYY-MM-DD) date.
	 */
	const RECORD_DATE_OPTION_KEY = 'woocommerce_sales_record_date';

	/**
	 * Option name for the sales record amount.
	 */
	const RECORD_AMOUNT_OPTION_KEY = 'woocommerce_sales_record_amount';

	/**
	 * Returns the total of yesterday's sales.
	 *
	 * @param string $date Date for sales to sum (i.e. YYYY-MM-DD).
	 * @return floatval
	 */
	public static function sum_sales_for_date( $date ) {
		$order_query = new \WC_Order_Query( array( 'date_created' => $date ) );
		$orders      = $order_query->get_orders();
		$total       = 0;

		foreach ( (array) $orders as $order ) {
			$total += $order->get_total();
		}

		return $total;
	}

	/**
	 * Possibly add a sales record note.
	 */
	public static function possibly_add_note() {
		/**
		 * Filter to allow for disabling sales record milestones.
		 *
		 * @since 3.7.0
		 *
		 * @param boolean default true
		 */
		$sales_record_notes_enabled = apply_filters( 'woocommerce_admin_sales_record_milestone_enabled', true );

		if ( ! $sales_record_notes_enabled ) {
			return;
		}

		$yesterday = gmdate( 'Y-m-d', current_time( 'timestamp', 0 ) - DAY_IN_SECONDS );
		$total     = self::sum_sales_for_date( $yesterday );

		// No sales yesterday? Bail.
		if ( 0 >= $total ) {
			return;
		}

		$record_date = get_option( self::RECORD_DATE_OPTION_KEY, '' );
		$record_amt  = floatval( get_option( self::RECORD_AMOUNT_OPTION_KEY, 0 ) );

		// No previous entry? Just enter what we have and return without generating a note.
		if ( empty( $record_date ) ) {
			update_option( self::RECORD_DATE_OPTION_KEY, $yesterday );
			update_option( self::RECORD_AMOUNT_OPTION_KEY, $total );
				return;
		}

		// Otherwise, if yesterdays total bested the record, update AND generate a note.
		if ( $total > $record_amt ) {
			update_option( self::RECORD_DATE_OPTION_KEY, $yesterday );
			update_option( self::RECORD_AMOUNT_OPTION_KEY, $total );

			// We only want one sales record note at any time in the inbox, so we delete any other first.
			Notes::delete_notes_with_name( self::NOTE_NAME );

			$note = self::get_note_with_record_data( $record_date, $record_amt, $yesterday, $total );
			$note->save();
		}
	}

	/**
	 * Get the note with record data.
	 *
	 * @param string $record_date record date Y-m-d.
	 * @param float  $record_amt record amount.
	 * @param string $yesterday yesterday's date Y-m-d.
	 * @param string $total total sales for yesterday.
	 *
	 * @return Note
	 */
	public static function get_note_with_record_data( $record_date, $record_amt, $yesterday, $total ) {
		// Use F jS (March 7th) format for English speaking countries.
		if ( substr( get_user_locale(), 0, 2 ) === 'en' ) {
			$date_format = 'F jS';
		} else {
			// otherwise, fallback to the system date format.
			$date_format = get_option( 'date_format' );
		}

		$formatted_yesterday   = date_i18n( $date_format, strtotime( $yesterday ) );
		$formatted_total       = html_entity_decode( wp_strip_all_tags( wc_price( $total ) ) );
		$formatted_record_date = date_i18n( $date_format, strtotime( $record_date ) );
		$formatted_record_amt  = html_entity_decode( wp_strip_all_tags( wc_price( $record_amt ) ) );

		$content = sprintf(
			/* translators: 1 and 4: Date (e.g. October 16th), 2 and 3: Amount (e.g. $160.00) */
			__( 'Woohoo, %1$s was your record day for sales! Net sales was %2$s beating the previous record of %3$s set on %4$s.', 'woocommerce' ),
			$formatted_yesterday,
			$formatted_total,
			$formatted_record_amt,
			$formatted_record_date
		);

		$content_data = (object) array(
			'old_record_date' => $record_date,
			'old_record_amt'  => $record_amt,
			'new_record_date' => $yesterday,
			'new_record_amt'  => $total,
		);

		$report_url = '?page=wc-admin&path=/analytics/revenue&period=custom&compare=previous_year&after=' . $yesterday . '&before=' . $yesterday;

		// And now, create our new note.
		$note = new Note();
		$note->set_title( __( 'New sales record!', 'woocommerce' ) );
		$note->set_content( $content );
		$note->set_content_data( $content_data );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action( 'view-report', __( 'View report', 'woocommerce' ), $report_url );

		return $note;
	}

	/**
	 * Get the note. This is used for localizing the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		$note = Notes::get_note_by_name( self::NOTE_NAME );
		if ( ! $note ) {
			return false;
		}
		$content_data = $note->get_content_data();
		return self::get_note_with_record_data(
			$content_data->old_record_date,
			$content_data->old_record_amt,
			$content_data->new_record_date,
			$content_data->new_record_amt
		);
	}
}
Internal/Admin/Notes/OnlineClothingStore.php000064400000005271152333011040015077 0ustar00<?php
/**
 * WooCommerce Admin: Start your online clothing store.
 *
 * Adds a note to ask the client if they are considering starting an online
 * clothing store.
 */

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;

/**
 * Online_Clothing_Store.
 */
class OnlineClothingStore {
	/**
	 * Note traits.
	 */
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-online-clothing-store';

	/**
	 * Returns whether the industries includes fashion-apparel-accessories.
	 *
	 * @param array $industries The industries to search.
	 *
	 * @return bool Whether the industries includes fashion-apparel-accessories.
	 */
	private static function is_in_fashion_industry( $industries ) {
		foreach ( $industries as $industry ) {
			if ( 'fashion-apparel-accessories' === $industry['slug'] ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Get the note.
	 *
	 * @return Note
	 */
	public static function get_note() {
		// We want to show the note after two days.
		if ( ! self::is_wc_admin_active_in_date_range( 'week-1', 2 * DAY_IN_SECONDS ) ) {
			return;
		}

		$onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() );

		// Confirm that $onboarding_profile is set.
		if ( empty( $onboarding_profile ) ) {
			return;
		}

		// Make sure that the person who filled out the OBW was not setting up
		// the store for their customer/client.
		if (
			! isset( $onboarding_profile['setup_client'] ) ||
			$onboarding_profile['setup_client']
		) {
			return;
		}

		// We need to show the notification when the industry is
		// fashion/apparel/accessories.
		if ( ! isset( $onboarding_profile['industry'] ) ) {
			return;
		}
		if ( ! self::is_in_fashion_industry( $onboarding_profile['industry'] ) ) {
			return;
		}

		$note = new Note();
		$note->set_title( __( 'Start your online clothing store', 'woocommerce' ) );
		$note->set_content( __( 'Starting a fashion website is exciting but it may seem overwhelming as well. In this article, we\'ll walk you through the setup process, teach you to create successful product listings, and show you how to market to your ideal audience.', 'woocommerce' ) );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_content_data( (object) array() );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'online-clothing-store',
			__( 'Learn more', 'woocommerce' ),
			'https://woocommerce.com/posts/starting-an-online-clothing-store/?utm_source=inbox&utm_medium=product',
			Note::E_WC_ADMIN_NOTE_ACTIONED
		);
		return $note;
	}
}
Internal/Admin/Events.php000064400000017532152333011040011325 0ustar00<?php
/**
 * Handle cron events.
 */

namespace Automattic\WooCommerce\Internal\Admin;

defined( 'ABSPATH' ) || exit;

use \Automattic\WooCommerce\Admin\Features\Features;
use \Automattic\WooCommerce\Admin\RemoteInboxNotifications\DataSourcePoller;
use \Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsEngine;
use \Automattic\WooCommerce\Internal\Admin\Notes\AddFirstProduct;
use \Automattic\WooCommerce\Internal\Admin\Notes\ChoosingTheme;
use \Automattic\WooCommerce\Internal\Admin\Notes\CouponPageMoved;
use \Automattic\WooCommerce\Internal\Admin\Notes\CustomizeStoreWithBlocks;
use \Automattic\WooCommerce\Internal\Admin\Notes\CustomizingProductCatalog;
use \Automattic\WooCommerce\Internal\Admin\Notes\EditProductsOnTheMove;
use \Automattic\WooCommerce\Internal\Admin\Notes\EUVATNumber;
use \Automattic\WooCommerce\Internal\Admin\Notes\FirstProduct;
use \Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins;
use \Automattic\WooCommerce\Internal\Admin\Notes\LaunchChecklist;
use \Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration;
use \Automattic\WooCommerce\Internal\Admin\Notes\ManageOrdersOnTheGo;
use \Automattic\WooCommerce\Internal\Admin\Notes\MarketingJetpack;
use \Automattic\WooCommerce\Internal\Admin\Notes\MerchantEmailNotifications;
use \Automattic\WooCommerce\Internal\Admin\Notes\MigrateFromShopify;
use \Automattic\WooCommerce\Internal\Admin\Notes\MobileApp;
use \Automattic\WooCommerce\Internal\Admin\Notes\NewSalesRecord;
use \Automattic\WooCommerce\Internal\Admin\Notes\OnboardingPayments;
use \Automattic\WooCommerce\Internal\Admin\Notes\OnlineClothingStore;
use \Automattic\WooCommerce\Internal\Admin\Notes\OrderMilestones;
use \Automattic\WooCommerce\Internal\Admin\Notes\PaymentsMoreInfoNeeded;
use \Automattic\WooCommerce\Internal\Admin\Notes\PaymentsRemindMeLater;
use \Automattic\WooCommerce\Internal\Admin\Notes\PerformanceOnMobile;
use \Automattic\WooCommerce\Internal\Admin\Notes\PersonalizeStore;
use \Automattic\WooCommerce\Internal\Admin\Notes\RealTimeOrderAlerts;
use \Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses;
use \Automattic\WooCommerce\Internal\Admin\Notes\TestCheckout;
use \Automattic\WooCommerce\Internal\Admin\Notes\TrackingOptIn;
use \Automattic\WooCommerce\Internal\Admin\Notes\UnsecuredReportFiles;
use \Automattic\WooCommerce\Internal\Admin\Notes\WelcomeToWooCommerceForStoreUsers;
use \Automattic\WooCommerce\Internal\Admin\Notes\WooCommercePayments;
use \Automattic\WooCommerce\Internal\Admin\Notes\WooCommerceSubscriptions;
use \Automattic\WooCommerce\Internal\Admin\Notes\WooSubscriptionsNotes;
use \Automattic\WooCommerce\Internal\Admin\Schedulers\MailchimpScheduler;
use \Automattic\WooCommerce\Admin\Notes\Note;

/**
 * Events Class.
 */
class Events {
	/**
	 * The single instance of the class.
	 *
	 * @var object
	 */
	protected static $instance = null;

	/**
	 * Constructor
	 *
	 * @return void
	 */
	protected function __construct() {}

	/**
	 * Array of note class to be added or updated.
	 *
	 * @var array
	 */
	private static $note_classes_to_added_or_updated = array(
		AddFirstProduct::class,
		ChoosingTheme::class,
		CustomizeStoreWithBlocks::class,
		CustomizingProductCatalog::class,
		EditProductsOnTheMove::class,
		EUVATNumber::class,
		FirstProduct::class,
		LaunchChecklist::class,
		MagentoMigration::class,
		ManageOrdersOnTheGo::class,
		MarketingJetpack::class,
		MigrateFromShopify::class,
		MobileApp::class,
		NewSalesRecord::class,
		OnboardingPayments::class,
		OnlineClothingStore::class,
		PaymentsMoreInfoNeeded::class,
		PaymentsRemindMeLater::class,
		PerformanceOnMobile::class,
		PersonalizeStore::class,
		RealTimeOrderAlerts::class,
		TestCheckout::class,
		TrackingOptIn::class,
		WooCommercePayments::class,
		WooCommerceSubscriptions::class,
	);

	/**
	 * The other note classes that are added in other places.
	 *
	 * @var array
	 */
	private static $other_note_classes = array(
		CouponPageMoved::class,
		InstallJPAndWCSPlugins::class,
		OrderMilestones::class,
		SellingOnlineCourses::class,
		UnsecuredReportFiles::class,
		WelcomeToWooCommerceForStoreUsers::class,
		WooSubscriptionsNotes::class,
	);


	/**
	 * Get class instance.
	 *
	 * @return object Instance.
	 */
	final public static function instance() {
		if ( null === static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Cron event handlers.
	 */
	public function init() {
		add_action( 'wc_admin_daily', array( $this, 'do_wc_admin_daily' ) );
		add_filter( 'woocommerce_get_note_from_db', array( $this, 'get_note_from_db' ), 10, 1 );

		// Initialize the WC_Notes_Refund_Returns Note to attach hook.
		\WC_Notes_Refund_Returns::init();
	}

	/**
	 * Daily events to run.
	 *
	 * Note: Order_Milestones::possibly_add_note is hooked to this as well.
	 */
	public function do_wc_admin_daily() {
		$this->possibly_add_notes();
		$this->possibly_delete_notes();
		$this->possibly_update_notes();

		if ( $this->is_remote_inbox_notifications_enabled() ) {
			DataSourcePoller::get_instance()->read_specs_from_data_sources();
			RemoteInboxNotificationsEngine::run();
		}

		if ( $this->is_merchant_email_notifications_enabled() ) {
			MerchantEmailNotifications::run();
		}

		if ( Features::is_enabled( 'onboarding' ) ) {
			( new MailchimpScheduler() )->run();
		}
	}

	/**
	 * Get note.
	 *
	 * @param Note $note_from_db The note object from the database.
	 */
	public function get_note_from_db( $note_from_db ) {
		if ( ! $note_from_db instanceof Note || get_user_locale() === $note_from_db->get_locale() ) {
			return $note_from_db;
		}

		$note_classes = array_merge( self::$note_classes_to_added_or_updated, self::$other_note_classes );
		foreach ( $note_classes as $note_class ) {
			if ( defined( "$note_class::NOTE_NAME" ) && $note_class::NOTE_NAME === $note_from_db->get_name() ) {
				$note_from_class = method_exists( $note_class, 'get_note' ) ? $note_class::get_note() : null;

				if ( $note_from_class instanceof Note ) {
					$note = clone $note_from_db;
					$note->set_title( $note_from_class->get_title() );
					$note->set_content( $note_from_class->get_content() );
					$note->set_actions( $note_from_class->get_actions() );
					return $note;
				}
				break;
			}
		}
		return $note_from_db;
	}

	/**
	 * Adds notes that should be added.
	 */
	protected function possibly_add_notes() {
		foreach ( self::$note_classes_to_added_or_updated as $note_class ) {
			if ( method_exists( $note_class, 'possibly_add_note' ) ) {
				$note_class::possibly_add_note();
			}
		}
	}

	/**
	 * Deletes notes that should be deleted.
	 */
	protected function possibly_delete_notes() {
		PaymentsRemindMeLater::delete_if_not_applicable();
		PaymentsMoreInfoNeeded::delete_if_not_applicable();
	}

	/**
	 * Updates notes that should be updated.
	 */
	protected function possibly_update_notes() {
		foreach ( self::$note_classes_to_added_or_updated as $note_class ) {
			if ( method_exists( $note_class, 'possibly_update_note' ) ) {
				$note_class::possibly_update_note();
			}
		}
	}

	/**
	 * Checks if remote inbox notifications are enabled.
	 *
	 * @return bool Whether remote inbox notifications are enabled.
	 */
	protected function is_remote_inbox_notifications_enabled() {
		// Check if the feature flag is disabled.
		if ( ! Features::is_enabled( 'remote-inbox-notifications' ) ) {
			return false;
		}

		// Check if the site has opted out of marketplace suggestions.
		if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) !== 'yes' ) {
			return false;
		}

		// All checks have passed.
		return true;
	}

	/**
	 * Checks if merchant email notifications are enabled.
	 *
	 * @return bool Whether merchant email notifications are enabled.
	 */
	protected function is_merchant_email_notifications_enabled() {
		// Check if the feature flag is disabled.
		if ( get_option( 'woocommerce_merchant_email_notifications', 'no' ) !== 'yes' ) {
			return false;
		}

		// All checks have passed.
		return true;
	}
}
Internal/Admin/CategoryLookup.php000064400000017764152333011040013037 0ustar00<?php
/**
 * Keeps the product category lookup table in sync with live data.
 */

namespace Automattic\WooCommerce\Internal\Admin;

defined( 'ABSPATH' ) || exit;

/**
 * \Automattic\WooCommerce\Internal\Admin\CategoryLookup class.
 */
class CategoryLookup {

	/**
	 * Stores changes to categories we need to sync.
	 *
	 * @var array
	 */
	protected $edited_product_cats = array();

	/**
	 * The single instance of the class.
	 *
	 * @var object
	 */
	protected static $instance = null;

	/**
	 * Constructor
	 *
	 * @return void
	 */
	protected function __construct() {}

	/**
	 * Get class instance.
	 *
	 * @return object Instance.
	 */
	final public static function instance() {
		if ( null === static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Init hooks.
	 */
	public function init() {
		add_action( 'generate_category_lookup_table', array( $this, 'regenerate' ) );
		add_action( 'edit_product_cat', array( $this, 'before_edit' ), 99 );
		add_action( 'edited_product_cat', array( $this, 'on_edit' ), 99 );
		add_action( 'created_product_cat', array( $this, 'on_create' ), 99 );
		add_action( 'init', array( $this, 'define_category_lookup_tables_in_wpdb' ) );
	}

	/**
	 * Regenerate all lookup table data.
	 */
	public function regenerate() {
		global $wpdb;

		$wpdb->query( "TRUNCATE TABLE $wpdb->wc_category_lookup" );

		$terms = get_terms(
			'product_cat',
			array(
				'hide_empty' => false,
				'fields'     => 'id=>parent',
			)
		);

		$hierarchy = array();
		$inserts   = array();

		$this->unflatten_terms( $hierarchy, $terms, 0 );
		$this->get_term_insert_values( $inserts, $hierarchy );

		if ( ! $inserts ) {
			return;
		}

		$insert_string = implode(
			'),(',
			array_map(
				function( $item ) {
					return implode( ',', $item );
				},
				$inserts
			)
		);

		$wpdb->query( "INSERT IGNORE INTO $wpdb->wc_category_lookup (category_tree_id,category_id) VALUES ({$insert_string})" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Store edits so we know when the parent ID changes.
	 *
	 * @param int $category_id Term ID being edited.
	 */
	public function before_edit( $category_id ) {
		$category                                  = get_term( $category_id, 'product_cat' );
		$this->edited_product_cats[ $category_id ] = $category->parent;
	}

	/**
	 * When a product category gets edited, see if we need to sync the table.
	 *
	 * @param int $category_id Term ID being edited.
	 */
	public function on_edit( $category_id ) {
		global $wpdb;

		if ( ! isset( $this->edited_product_cats[ $category_id ] ) ) {
			return;
		}

		$category_object = get_term( $category_id, 'product_cat' );
		$prev_parent     = $this->edited_product_cats[ $category_id ];
		$new_parent      = $category_object->parent;

		// No edits - no need to modify relationships.
		if ( $prev_parent === $new_parent ) {
			return;
		}

		$this->delete( $category_id, $prev_parent );
		$this->update( $category_id );
	}

	/**
	 * When a product category gets created, add a new lookup row.
	 *
	 * @param int $category_id Term ID being created.
	 */
	public function on_create( $category_id ) {
		// If WooCommerce is being installed on a multisite, lookup tables haven't been created yet.
		if ( 'yes' === get_transient( 'wc_installing' ) ) {
			return;
		}

		$this->update( $category_id );
	}

	/**
	 * Delete lookup table data from a tree.
	 *
	 * @param int $category_id Category ID to delete.
	 * @param int $category_tree_id Tree to delete from.
	 * @return void
	 */
	protected function delete( $category_id, $category_tree_id ) {
		global $wpdb;

		if ( ! $category_tree_id ) {
			return;
		}

		$ancestors   = get_ancestors( $category_tree_id, 'product_cat', 'taxonomy' );
		$ancestors[] = $category_tree_id;
		$children    = get_term_children( $category_id, 'product_cat' );
		$children[]  = $category_id;
		$id_list     = implode( ',', array_map( 'intval', array_unique( array_filter( $children ) ) ) );

		foreach ( $ancestors as $ancestor ) {
			$wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->wc_category_lookup WHERE category_tree_id = %d AND category_id IN ({$id_list})", $ancestor ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		}
	}

	/**
	 * Updates lookup table data for a category by ID.
	 *
	 * @param int $category_id Category ID to update.
	 */
	protected function update( $category_id ) {
		global $wpdb;

		$ancestors    = get_ancestors( $category_id, 'product_cat', 'taxonomy' );
		$children     = get_term_children( $category_id, 'product_cat' );
		$inserts      = array();
		$inserts[]    = $this->get_insert_sql( $category_id, $category_id );
		$children_ids = array_map( 'intval', array_unique( array_filter( $children ) ) );

		foreach ( $ancestors as $ancestor ) {
			$inserts[] = $this->get_insert_sql( $category_id, $ancestor );

			foreach ( $children_ids as $child_category_id ) {
				$inserts[] = $this->get_insert_sql( $child_category_id, $ancestor );
			}
		}

		$insert_string = implode( ',', $inserts );

		$wpdb->query( "INSERT IGNORE INTO $wpdb->wc_category_lookup (category_id, category_tree_id) VALUES {$insert_string}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Get category lookup table values to insert.
	 *
	 * @param int $category_id Category ID to insert.
	 * @param int $category_tree_id Tree to insert into.
	 * @return string
	 */
	protected function get_insert_sql( $category_id, $category_tree_id ) {
		global $wpdb;
		return $wpdb->prepare( '(%d,%d)', $category_id, $category_tree_id );
	}

	/**
	 * Used to construct insert query recursively.
	 *
	 * @param  array $inserts Array of data to insert.
	 * @param  array $terms   Terms to insert.
	 * @param  array $parents Parent IDs the terms belong to.
	 */
	protected function get_term_insert_values( &$inserts, $terms, $parents = array() ) {
		foreach ( $terms as $term ) {
			$insert_parents = array_merge( array( $term['term_id'] ), $parents );

			foreach ( $insert_parents as $parent ) {
				$inserts[] = array(
					$parent,
					$term['term_id'],
				);
			}

			$this->get_term_insert_values( $inserts, $term['descendants'], $insert_parents );
		}
	}

	/**
	 * Convert flat terms array into nested array.
	 *
	 * @param array   $hierarchy Array to put terms into.
	 * @param array   $terms Array of terms (id=>parent).
	 * @param integer $parent Parent ID.
	 */
	protected function unflatten_terms( &$hierarchy, &$terms, $parent = 0 ) {
		foreach ( $terms as $term_id => $parent_id ) {
			if ( (int) $parent_id === $parent ) {
				$hierarchy[ $term_id ] = array(
					'term_id'     => $term_id,
					'descendants' => array(),
				);
				unset( $terms[ $term_id ] );
			}
		}
		foreach ( $hierarchy as $term_id => $terms_array ) {
			$this->unflatten_terms( $hierarchy[ $term_id ]['descendants'], $terms, $term_id );
		}
	}

	/**
	 * Get category descendants.
	 *
	 * @param int $category_id The category ID to lookup.
	 * @return array
	 */
	protected function get_descendants( $category_id ) {
		global $wpdb;

		return wp_parse_id_list(
			$wpdb->get_col(
				$wpdb->prepare(
					"SELECT category_id FROM $wpdb->wc_category_lookup WHERE category_tree_id = %d",
					$category_id
				)
			)
		);
	}

	/**
	 * Return all ancestor category ids for a category.
	 *
	 * @param int $category_id The category ID to lookup.
	 * @return array
	 */
	protected function get_ancestors( $category_id ) {
		global $wpdb;

		return wp_parse_id_list(
			$wpdb->get_col(
				$wpdb->prepare(
					"SELECT category_tree_id FROM $wpdb->wc_category_lookup WHERE category_id = %d",
					$category_id
				)
			)
		);
	}

	/**
	 * Add category lookup table to $wpdb object.
	 */
	public static function define_category_lookup_tables_in_wpdb() {
		global $wpdb;

		// List of tables without prefixes.
		$tables = array(
			'wc_category_lookup' => 'wc_category_lookup',
		);

		foreach ( $tables as $name => $table ) {
			$wpdb->$name    = $wpdb->prefix . $table;
			$wpdb->tables[] = $table;
		}
	}
}
Internal/Admin/WcPayWelcomePage.php000064400000016460152333011040013214 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments;
use Automattic\WooCommerce\Admin\WCAdminHelper;

/**
 * Class WCPayWelcomePage
 *
 * @package Automattic\WooCommerce\Admin\Features
 */
class WcPayWelcomePage {

	const EXPERIMENT_NAME = 'woocommerce_payments_menu_promo_us_2022';
	const OTHER_GATEWAYS  = [
		'affirm',
		'afterpay',
		'amazon_payments_advanced_express',
		'amazon_payments_advanced',
		'authorize_net_cim_credit_card',
		'authorize_net_cim_echeck',
		'bacs',
		'bambora_credit_card',
		'braintree_credit_card',
		'braintree_paypal',
		'chase_paymentech',
		'cybersource_credit_card',
		'elavon_converge_credit_card',
		'elavon_converge_echeck',
		'gocardless',
		'intuit_payments_credit_card',
		'intuit_payments_echeck',
		'kco',
		'klarna_payments',
		'payfast',
		'paypal',
		'paytrace',
		'ppcp-gateway',
		'psigate',
		'sagepaymentsusaapi',
		'square_credit_card',
		'stripe_alipay',
		'stripe_multibanco',
		'stripe',
		'trustcommerce',
		'usa_epay_credit_card',
	];

	/**
	 * WCPayWelcomePage constructor.
	 */
	public function __construct() {
		add_action( 'admin_menu', array( $this, 'register_payments_welcome_page' ) );
	}

	/**
	 * Registers the WooCommerce Payments welcome page.
	 */
	public function register_payments_welcome_page() {
		global $menu;

		// WC Payment must not be installed.
		if ( WooCommercePayments::is_installed() ) {
			return;
		}

		// Live store for at least 90 days.
		if ( ! WCAdminHelper::is_wc_admin_active_for( DAY_IN_SECONDS * 90 ) ) {
			return;
		}

		// Must be a US based business.
		if ( WC()->countries->get_base_country() !== 'US' ) {
			return;
		}

		// Has another payment gateway installed.
		if ( ! $this->is_another_payment_gateway_installed() ) {
			return;
		}

		// No existing WCPay account.
		if ( $this->has_wcpay_account() ) {
			return;
		}

		// Suggestions may be disabled via a setting.
		if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) === 'no' ) {
			return;
		}

		/**
		 * Filter allow marketplace suggestions.
		 *
		 * User can disabled all suggestions via filter.
		 *
		 * @since 3.6.0
		 */
		if ( ! apply_filters( 'woocommerce_allow_marketplace_suggestions', true ) ) {
			return;
		}

		// Manually dismissed.
		if ( get_option( 'wc_calypso_bridge_payments_dismissed', 'no' ) === 'yes' ) {
			return;
		}

		// Users must be in the experiment.
		if ( ! $this->is_user_in_treatment_mode() ) {
			return;
		}

		$menu_icon = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgdmVyc2lvbj0iMS4xIgogICBpZD0ic3ZnNjciCiAgIHNvZGlwb2RpOmRvY25hbWU9IndjcGF5X21lbnVfaWNvbi5zdmciCiAgIHdpZHRoPSI4NTIiCiAgIGhlaWdodD0iNjg0IgogICBpbmtzY2FwZTp2ZXJzaW9uPSIxLjEgKGM0ZThmOWUsIDIwMjEtMDUtMjQpIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM3MSIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzY5IgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiCiAgICAgc2hvd2dyaWQ9ImZhbHNlIgogICAgIGZpdC1tYXJnaW4tdG9wPSIwIgogICAgIGZpdC1tYXJnaW4tbGVmdD0iMCIKICAgICBmaXQtbWFyZ2luLXJpZ2h0PSIwIgogICAgIGZpdC1tYXJnaW4tYm90dG9tPSIwIgogICAgIGlua3NjYXBlOnpvb209IjI1NiIKICAgICBpbmtzY2FwZTpjeD0iLTg0Ljg1NzQyMiIKICAgICBpbmtzY2FwZTpjeT0iLTgzLjI5NDkyMiIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzMTIiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTA4MSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMTE2IgogICAgIGlua3NjYXBlOndpbmRvdy15PSIyMDIiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmc2NyIgLz4KICA8cGF0aAogICAgIHRyYW5zZm9ybT0ic2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtODUwLCAwKSIKICAgICBkPSJNIDc2OCw4NiBWIDU5OCBIIDg0IFYgODYgWiBtIDAsNTk4IGMgNDgsMCA4NCwtMzggODQsLTg2IFYgODYgQyA4NTIsMzggODE2LDAgNzY4LDAgSCA4NCBDIDM2LDAgMCwzOCAwLDg2IHYgNTEyIGMgMCw0OCAzNiw4NiA4NCw4NiB6IE0gMzg0LDEyOCB2IDQ0IGggLTg2IHYgODQgaCAxNzAgdiA0NCBIIDM0MCBjIC0yNCwwIC00MiwxOCAtNDIsNDIgdiAxMjggYyAwLDI0IDE4LDQyIDQyLDQyIGggNDQgdiA0NCBoIDg0IHYgLTQ0IGggODYgViA0MjggSCAzODQgdiAtNDQgaCAxMjggYyAyNCwwIDQyLC0xOCA0MiwtNDIgViAyMTQgYyAwLC0yNCAtMTgsLTQyIC00MiwtNDIgaCAtNDQgdiAtNDQgeiIKICAgICBmaWxsPSIjYTJhYWIyIgogICAgIGlkPSJwYXRoNjUiIC8+Cjwvc3ZnPgo=';

		$menu_data = array(
			'id'       => 'wc-calypso-bridge-payments-welcome-page',
			'title'    => __( 'Payments', 'woocommerce' ),
			'path'     => '/wc-pay-welcome-page',
			'position' => '56',
			'nav_args' => [
				'title'        => __( 'WooCommerce Payments', 'woocommerce' ),
				'is_category'  => false,
				'menuId'       => 'plugins',
				'is_top_level' => true,
			],
			'icon'     => $menu_icon,
		);

		wc_admin_register_page( $menu_data );

		// Registering a top level menu via wc_admin_register_page doesn't work when the new
		// nav is enabled. The new nav disabled everything, except the 'WooCommerce' menu.
		// We need to register this menu via add_menu_page so that it doesn't become a child of
		// WooCommerce menu.
		if ( get_option( 'woocommerce_navigation_enabled', 'no' ) === 'yes' ) {
			$menu_with_nav_data = array(
				__( 'Payments', 'woocommerce' ),
				__( 'Payments', 'woocommerce' ),
				'view_woocommerce_reports',
				'admin.php?page=wc-admin&path=/wc-pay-welcome-page',
				null,
				$menu_icon,
				56,
			);

			call_user_func_array( 'add_menu_page', $menu_with_nav_data );
		}

		// Add badge.
		foreach ( $menu as $index => $menu_item ) {
			if ( 'wc-admin&path=/wc-pay-welcome-page' === $menu_item[2]
					|| 'admin.php?page=wc-admin&path=/wc-pay-welcome-page' === $menu_item[2] ) {
				//phpcs:ignore
				$menu[ $index ][0] .= ' <span class="wcpay-menu-badge awaiting-mod count-1"><span class="plugin-count">1</span></span>';
			}
		}
	}

	/**
	 * Whether a WCPay account exists. By checking account data cache.
	 *
	 * @return boolean
	 */
	private function has_wcpay_account(): bool {
		$account_data = get_option( 'wcpay_account_data' );
		return isset( $account_data['data'] ) && is_array( $account_data['data'] ) && ! empty( $account_data['data'] );
	}

	/**
	 * Checks if user is in the experiment.
	 *
	 * @return bool Whether the user is in the treatment group.
	 */
	private function is_user_in_treatment_mode() {
		$anon_id        = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : '';
		$allow_tracking = get_option( 'woocommerce_allow_tracking' ) === 'yes';
		$abtest         = new \WooCommerce\Admin\Experimental_Abtest(
			$anon_id,
			'woocommerce',
			$allow_tracking
		);

		return $abtest->get_variation( self::EXPERIMENT_NAME ) === 'treatment';
	}

	/**
	 * Checks if there is another payment gateway installed using a static list of US gateways from WC Store.
	 *
	 * @return bool Whether there is another payment gateway installed.
	 */
	private function is_another_payment_gateway_installed() {
		$available_gateways = wp_list_pluck( WC()->payment_gateways()->get_available_payment_gateways(), 'id' );

		foreach ( $available_gateways as $gateway ) {
			if ( in_array( $gateway, self::OTHER_GATEWAYS, true ) ) {
				return true;
			}
		}

		return false;
	}

}
Internal/Admin/Translations.php000064400000022132152333011040012532 0ustar00<?php
/**
 * Register the scripts, and handles items needed for managing translations within WooCommerce Admin.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Internal\Admin\Loader;

/**
 * Translations Class.
 */
class Translations {

	/**
	 * Class instance.
	 *
	 * @var Translations instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor.
	 * Hooks added here should be removed in `wc_admin_initialize` via the feature plugin.
	 */
	public function __construct() {
		add_action( 'admin_enqueue_scripts', array( $this, 'potentially_load_translation_script_file' ), 15 );

		// Combine JSON translation files (from chunks) when language packs are updated.
		add_action( 'upgrader_process_complete', array( $this, 'combine_translation_chunk_files' ), 10, 2 );

		// Handler for WooCommerce and WooCommerce Admin plugin activation.
		add_action( 'woocommerce_activated_plugin', array( $this, 'potentially_generate_translation_strings' ) );
		add_action( 'activated_plugin', array( $this, 'potentially_generate_translation_strings' ) );
	}

	/**
	 * Generate a filename to cache translations from JS chunks.
	 *
	 * @param string $domain Text domain.
	 * @param string $locale Locale being retrieved.
	 * @return string Filename.
	 */
	private function get_combined_translation_filename( $domain, $locale ) {
		$filename = implode( '-', array( $domain, $locale, WC_ADMIN_APP ) ) . '.json';

		return $filename;
	}

	/**
	 * Find and combine translation chunk files.
	 *
	 * Only targets files that aren't represented by a registered script (e.g. not passed to wp_register_script()).
	 *
	 * @param string $lang_dir Path to language files.
	 * @param string $domain Text domain.
	 * @param string $locale Locale being retrieved.
	 * @return array Combined translation chunk data.
	 */
	private function get_translation_chunk_data( $lang_dir, $domain, $locale ) {
		// So long as this function is called during the 'upgrader_process_complete' action,
		// the filesystem object should be hooked up.
		global $wp_filesystem;

		// Grab all JSON files in the current language pack.
		$json_i18n_filenames       = glob( $lang_dir . $domain . '-' . $locale . '-*.json' );
		$combined_translation_data = array();

		if ( false === $json_i18n_filenames ) {
			return $combined_translation_data;
		}

		foreach ( $json_i18n_filenames as $json_filename ) {
			if ( ! $wp_filesystem->is_readable( $json_filename ) ) {
				continue;
			}

			$file_contents = $wp_filesystem->get_contents( $json_filename );
			$chunk_data    = \json_decode( $file_contents, true );

			if ( empty( $chunk_data ) ) {
				continue;
			}

			if ( ! isset( $chunk_data['comment']['reference'] ) ) {
				continue;
			}

			$reference_file = $chunk_data['comment']['reference'];

			// Only combine "app" files (not scripts registered with WP).
			if (
				false === strpos( $reference_file, WC_ADMIN_DIST_JS_FOLDER . 'app/index.js' ) &&
				false === strpos( $reference_file, WC_ADMIN_DIST_JS_FOLDER . 'chunks/' )
			) {
				continue;
			}

			if ( empty( $combined_translation_data ) ) {
				// Use the first translation file as the base structure.
				$combined_translation_data = $chunk_data;
			} else {
				// Combine all messages from all chunk files.
				$combined_translation_data['locale_data']['messages'] = array_merge(
					$combined_translation_data['locale_data']['messages'],
					$chunk_data['locale_data']['messages']
				);
			}
		}

		// Remove inaccurate reference comment.
		unset( $combined_translation_data['comment'] );

		return $combined_translation_data;
	}

	/**
	 * Combine and save translations for a specific locale.
	 *
	 * Note that this assumes \WP_Filesystem is already initialized with write access.
	 *
	 * @param string $language_dir Path to language files.
	 * @param string $plugin_domain Text domain.
	 * @param string $locale Locale being retrieved.
	 */
	private function build_and_save_translations( $language_dir, $plugin_domain, $locale ) {
		global $wp_filesystem;
		$translations_from_chunks = $this->get_translation_chunk_data( $language_dir, $plugin_domain, $locale );

		if ( empty( $translations_from_chunks ) ) {
			return;
		}

		$cache_filename          = $this->get_combined_translation_filename( $plugin_domain, $locale );
		$chunk_translations_json = wp_json_encode( $translations_from_chunks );

		// Cache combined translations strings to a file.
		$wp_filesystem->put_contents( $language_dir . $cache_filename, $chunk_translations_json );
	}

	/**
	 * Combine translation chunks when plugin is activated.
	 *
	 * This function combines JSON translation data auto-extracted by GlotPress
	 * from Webpack-generated JS chunks into a single file. This is necessary
	 * since the JS chunks are not known to WordPress via wp_register_script()
	 * and wp_set_script_translations().
	 */
	private function generate_translation_strings() {
		$plugin_domain = explode( '/', plugin_basename( __FILE__ ) )[0];
		$locale        = determine_locale();
		$lang_dir      = WP_LANG_DIR . '/plugins/';

		// Bail early if not localized.
		if ( 'en_US' === $locale ) {
			return;
		}

		if ( ! function_exists( 'get_filesystem_method' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		$access_type = get_filesystem_method();
		if ( 'direct' === $access_type ) {
			\WP_Filesystem();
			$this->build_and_save_translations( $lang_dir, $plugin_domain, $locale );
		} else {
			// I'm reluctant to add support for other filesystems here as it would require
			// user's input on activating plugin - which I don't think is common.
			return;
		}
	}

	/**
	 * Loads the required translation scripts on the correct pages.
	 */
	public function potentially_load_translation_script_file() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}

		// Grab translation strings from Webpack-generated chunks.
		add_filter( 'load_script_translation_file', array( $this, 'load_script_translation_file' ), 10, 3 );
	}

	/**
	 * Load translation strings from language packs for dynamic imports.
	 *
	 * @param string $file File location for the script being translated.
	 * @param string $handle Script handle.
	 * @param string $domain Text domain.
	 *
	 * @return string New file location for the script being translated.
	 */
	public function load_script_translation_file( $file, $handle, $domain ) {
		// Make sure the main app script is being loaded.
		if ( WC_ADMIN_APP !== $handle ) {
			return $file;
		}

		// Make sure we're handing the correct domain (could be woocommerce or woocommerce-admin).
		$plugin_domain = explode( '/', plugin_basename( __FILE__ ) )[0];
		if ( $plugin_domain !== $domain ) {
			return $file;
		}

		$locale         = determine_locale();
		$cache_filename = $this->get_combined_translation_filename( $domain, $locale );

		return WP_LANG_DIR . '/plugins/' . $cache_filename;
	}

	/**
	 * Run when plugin is activated (can be WooCommerce or WooCommerce Admin).
	 *
	 * @param string $filename Activated plugin filename.
	 */
	public function potentially_generate_translation_strings( $filename ) {
		$plugin_domain           = explode( '/', plugin_basename( __FILE__ ) )[0];
		$activated_plugin_domain = explode( '/', $filename )[0];

		// Ensure we're only running only on activation hook that originates from our plugin.
		if ( $plugin_domain === $activated_plugin_domain ) {
			$this->generate_translation_strings();
		}
	}

	/**
	 * Combine translation chunks when files are updated.
	 *
	 * This function combines JSON translation data auto-extracted by GlotPress
	 * from Webpack-generated JS chunks into a single file that can be used in
	 * subsequent requests. This is necessary since the JS chunks are not known
	 * to WordPress via wp_register_script() and wp_set_script_translations().
	 *
	 * @param Language_Pack_Upgrader $instance Upgrader instance.
	 * @param array                  $hook_extra Info about the upgraded language packs.
	 */
	public function combine_translation_chunk_files( $instance, $hook_extra ) {
		if (
			! is_a( $instance, 'Language_Pack_Upgrader' ) ||
			! isset( $hook_extra['translations'] ) ||
			! is_array( $hook_extra['translations'] )
		) {
			return;
		}

		// Make sure we're handing the correct domain (could be woocommerce or woocommerce-admin).
		$plugin_domain = explode( '/', plugin_basename( __FILE__ ) )[0];
		$locales       = array();
		$language_dir  = WP_LANG_DIR . '/plugins/';

		// Gather the locales that were updated in this operation.
		foreach ( $hook_extra['translations'] as $translation ) {
			if (
				'plugin' === $translation['type'] &&
				$plugin_domain === $translation['slug']
			) {
				$locales[] = $translation['language'];
			}
		}

		// Build combined translation files for all updated locales.
		foreach ( $locales as $locale ) {
			// So long as this function is hooked to the 'upgrader_process_complete' action,
			// WP_Filesystem should be hooked up to be able to call build_and_save_translations.
			$this->build_and_save_translations( $language_dir, $plugin_domain, $locale );
		}
	}
}
Internal/Admin/Settings.php000064400000026610152333011040011656 0ustar00<?php
/**
 * WooCommerce Settings.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\API\Plugins;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore as OrdersDataStore;
use Automattic\WooCommerce\Admin\PluginsHelper;
use WC_Marketplace_Suggestions;

/**
 * Contains logic in regards to WooCommerce Admin Settings.
 */
class Settings {

	/**
	 * Class instance.
	 *
	 * @var Settings instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		// Old settings injection.
		add_filter( 'woocommerce_components_settings', array( $this, 'add_component_settings' ) );
		// New settings injection.
		add_filter( 'woocommerce_admin_shared_settings', array( $this, 'add_component_settings' ) );
		add_filter( 'woocommerce_settings_groups', array( $this, 'add_settings_group' ) );
		add_filter( 'woocommerce_settings-wc_admin', array( $this, 'add_settings' ) );
	}

	/**
	 * Format order statuses by removing a leading 'wc-' if present.
	 *
	 * @param array $statuses Order statuses.
	 * @return array formatted statuses.
	 */
	public static function get_order_statuses( $statuses ) {
		$formatted_statuses = array();
		foreach ( $statuses as $key => $value ) {
			$formatted_key                        = preg_replace( '/^wc-/', '', $key );
			$formatted_statuses[ $formatted_key ] = $value;
		}
		return $formatted_statuses;
	}

	/**
	 * Get all order statuses present in analytics tables that aren't registered.
	 *
	 * @return array Unregistered order statuses.
	 */
	private function get_unregistered_order_statuses() {
		$registered_statuses   = wc_get_order_statuses();
		$all_synced_statuses   = OrdersDataStore::get_all_statuses();
		$unregistered_statuses = array_diff( $all_synced_statuses, array_keys( $registered_statuses ) );
		$formatted_status_keys = self::get_order_statuses( array_fill_keys( $unregistered_statuses, '' ) );
		$formatted_statuses    = array_keys( $formatted_status_keys );

		return array_combine( $formatted_statuses, $formatted_statuses );
	}

	/**
	 * Return an object defining the currecy options for the site's current currency
	 *
	 * @return  array  Settings for the current currency {
	 *     Array of settings.
	 *
	 *     @type string $code       Currency code.
	 *     @type string $precision  Number of decimals.
	 *     @type string $symbol     Symbol for currency.
	 * }
	 */
	public static function get_currency_settings() {
		$code = get_woocommerce_currency();

		//phpcs:ignore
		return apply_filters(
			'wc_currency_settings',
			array(
				'code'              => $code,
				'precision'         => wc_get_price_decimals(),
				'symbol'            => html_entity_decode( get_woocommerce_currency_symbol( $code ) ),
				'symbolPosition'    => get_option( 'woocommerce_currency_pos' ),
				'decimalSeparator'  => wc_get_price_decimal_separator(),
				'thousandSeparator' => wc_get_price_thousand_separator(),
				'priceFormat'       => html_entity_decode( get_woocommerce_price_format() ),
			)
		);
	}

	/**
	 * Hooks extra neccessary data into the component settings array already set in WooCommerce core.
	 *
	 * @param array $settings Array of component settings.
	 * @return array Array of component settings.
	 */
	public function add_component_settings( $settings ) {
		if ( ! is_admin() ) {
			return $settings;
		}

		if ( ! function_exists( 'wc_blocks_container' ) ) {
			global $wp_locale;
			// inject data not available via older versions of wc_blocks/woo.
			$settings['orderStatuses'] = self::get_order_statuses( wc_get_order_statuses() );
			$settings['stockStatuses'] = self::get_order_statuses( wc_get_product_stock_status_options() );
			$settings['currency']      = self::get_currency_settings();
			$settings['locale']        = array(
				'siteLocale'    => isset( $settings['siteLocale'] )
					? $settings['siteLocale']
					: get_locale(),
				'userLocale'    => isset( $settings['l10n']['userLocale'] )
					? $settings['l10n']['userLocale']
					: get_user_locale(),
				'weekdaysShort' => isset( $settings['l10n']['weekdaysShort'] )
					? $settings['l10n']['weekdaysShort']
					: array_values( $wp_locale->weekday_abbrev ),
			);
		}

		//phpcs:ignore
		$preload_data_endpoints = apply_filters( 'woocommerce_component_settings_preload_endpoints', array() );
		if ( class_exists( 'Jetpack' ) ) {
			$preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection';
		}
		if ( ! empty( $preload_data_endpoints ) ) {
			$preload_data = array_reduce(
				array_values( $preload_data_endpoints ),
				'rest_preload_api_request'
			);
		}

		//phpcs:ignore
		$preload_options = apply_filters( 'woocommerce_admin_preload_options', array() );
		if ( ! empty( $preload_options ) ) {
			foreach ( $preload_options as $option ) {
				$settings['preloadOptions'][ $option ] = get_option( $option );
			}
		}

		//phpcs:ignore
		$preload_settings = apply_filters( 'woocommerce_admin_preload_settings', array() );
		if ( ! empty( $preload_settings ) ) {
			$setting_options = new \WC_REST_Setting_Options_V2_Controller();
			foreach ( $preload_settings as $group ) {
				$group_settings   = $setting_options->get_group_settings( $group );
				$preload_settings = array();
				foreach ( $group_settings as $option ) {
					if ( array_key_exists( 'id', $option ) && array_key_exists( 'value', $option ) ) {
						$preload_settings[ $option['id'] ] = $option['value'];
					}
				}
				$settings['preloadSettings'][ $group ] = $preload_settings;
			}
		}

		$user_controller = new \WP_REST_Users_Controller();
		$request         = new \WP_REST_Request();
		$request->set_query_params( array( 'context' => 'edit' ) );
		$user_response     = $user_controller->get_current_item( $request );
		$current_user_data = is_wp_error( $user_response ) ? (object) array() : $user_response->get_data();

		$settings['currentUserData']      = $current_user_data;
		$settings['reviewsEnabled']       = get_option( 'woocommerce_enable_reviews' );
		$settings['manageStock']          = get_option( 'woocommerce_manage_stock' );
		$settings['commentModeration']    = get_option( 'comment_moderation' );
		$settings['notifyLowStockAmount'] = get_option( 'woocommerce_notify_low_stock_amount' );

		/**
		 * Deprecate wcAdminAssetUrl as we no longer need it after The Merge.
		 * Use wcAssetUrl instead.
		 *
		 * @deprecated 6.7.0
		 * @var string
		 */
		$settings['wcAdminAssetUrl'] = WC_ADMIN_IMAGES_FOLDER_URL;
		$settings['wcVersion']       = WC_VERSION;
		$settings['siteUrl']         = site_url();
		$settings['shopUrl']         = get_permalink( wc_get_page_id( 'shop' ) );
		$settings['homeUrl']         = home_url();
		$settings['dateFormat']      = get_option( 'date_format' );
		$settings['timeZone']        = wc_timezone_string();
		$settings['plugins']         = array(
			'installedPlugins' => PluginsHelper::get_installed_plugin_slugs(),
			'activePlugins'    => Plugins::get_active_plugins(),
		);
		// Plugins that depend on changing the translation work on the server but not the client -
		// WooCommerce Branding is an example of this - so pass through the translation of
		// 'WooCommerce' to wcSettings.
		$settings['woocommerceTranslation'] = __( 'WooCommerce', 'woocommerce' );
		// We may have synced orders with a now-unregistered status.
		// E.g An extension that added statuses is now inactive or removed.
		$settings['unregisteredOrderStatuses'] = $this->get_unregistered_order_statuses();
		// The separator used for attributes found in Variation titles.
		//phpcs:ignore
		$settings['variationTitleAttributesSeparator'] = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', new \WC_Product() );

		if ( ! empty( $preload_data_endpoints ) ) {
			$settings['dataEndpoints'] = isset( $settings['dataEndpoints'] )
				? $settings['dataEndpoints']
				: array();
			foreach ( $preload_data_endpoints as $key => $endpoint ) {
				// Handle error case: rest_do_request() doesn't guarantee success.
				if ( empty( $preload_data[ $endpoint ] ) ) {
					$settings['dataEndpoints'][ $key ] = array();
				} else {
					$settings['dataEndpoints'][ $key ] = $preload_data[ $endpoint ]['body'];
				}
			}
		}
		$settings = $this->get_custom_settings( $settings );
		if ( PageController::is_embed_page() ) {
			$settings['embedBreadcrumbs'] = wc_admin_get_breadcrumbs();
		}

		$settings['allowMarketplaceSuggestions']      = WC_Marketplace_Suggestions::allow_suggestions();
		$settings['connectNonce']                     = wp_create_nonce( 'connect' );
		$settings['wcpay_welcome_page_connect_nonce'] = wp_create_nonce( 'wcpay-connect' );

		return $settings;
	}

	/**
	 * Register the admin settings for use in the WC REST API
	 *
	 * @param array $groups Array of setting groups.
	 * @return array
	 */
	public function add_settings_group( $groups ) {
		$groups[] = array(
			'id'          => 'wc_admin',
			'label'       => __( 'WooCommerce Admin', 'woocommerce' ),
			'description' => __( 'Settings for WooCommerce admin reporting.', 'woocommerce' ),
		);
		return $groups;
	}

	/**
	 * Add WC Admin specific settings
	 *
	 * @param array $settings Array of settings in wc admin group.
	 * @return array
	 */
	public function add_settings( $settings ) {
		$unregistered_statuses = $this->get_unregistered_order_statuses();
		$registered_statuses   = self::get_order_statuses( wc_get_order_statuses() );
		$all_statuses          = array_merge( $unregistered_statuses, $registered_statuses );

		$settings[] = array(
			'id'          => 'woocommerce_excluded_report_order_statuses',
			'option_key'  => 'woocommerce_excluded_report_order_statuses',
			'label'       => __( 'Excluded report order statuses', 'woocommerce' ),
			'description' => __( 'Statuses that should not be included when calculating report totals.', 'woocommerce' ),
			'default'     => array( 'pending', 'cancelled', 'failed' ),
			'type'        => 'multiselect',
			'options'     => $all_statuses,
		);
		$settings[] = array(
			'id'          => 'woocommerce_actionable_order_statuses',
			'option_key'  => 'woocommerce_actionable_order_statuses',
			'label'       => __( 'Actionable order statuses', 'woocommerce' ),
			'description' => __( 'Statuses that require extra action on behalf of the store admin.', 'woocommerce' ),
			'default'     => array( 'processing', 'on-hold' ),
			'type'        => 'multiselect',
			'options'     => $all_statuses,
		);
		$settings[] = array(
			'id'          => 'woocommerce_default_date_range',
			'option_key'  => 'woocommerce_default_date_range',
			'label'       => __( 'Default Date Range', 'woocommerce' ),
			'description' => __( 'Default Date Range', 'woocommerce' ),
			'default'     => 'period=month&compare=previous_year',
			'type'        => 'text',
		);
		return $settings;
	}

	/**
	 * Gets custom settings used for WC Admin.
	 *
	 * @param array $settings Array of settings to merge into.
	 * @return array
	 */
	private function get_custom_settings( $settings ) {
		$wc_rest_settings_options_controller = new \WC_REST_Setting_Options_Controller();
		$wc_admin_group_settings             = $wc_rest_settings_options_controller->get_group_settings( 'wc_admin' );
		$settings['wcAdminSettings']         = array();

		foreach ( $wc_admin_group_settings as $setting ) {
			if ( ! empty( $setting['id'] ) ) {
				$settings['wcAdminSettings'][ $setting['id'] ] = $setting['value'];
			}
		}
		return $settings;
	}
}
Internal/Admin/Orders/PostsRedirectionController.php000064400000010552152333011040016656 0ustar00<?php
namespace Automattic\WooCommerce\Internal\Admin\Orders;

use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;

/**
 * When {@see OrdersTableDataStore} is in use, this class takes care of redirecting admins from CPT-based URLs
 * to the new ones.
 */
class PostsRedirectionController {

	/**
	 * Instance of the PageController class.
	 *
	 * @var PageController
	 */
	private $page_controller;

	/**
	 * Constructor.
	 *
	 * @param PageController $page_controller Page controller instance. Used to generate links/URLs.
	 */
	public function __construct( PageController $page_controller ) {
		$this->page_controller = $page_controller;

		if ( ! wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) {
			return;
		}

		add_action(
			'load-edit.php',
			function() {
				$this->maybe_redirect_to_orders_page();
			}
		);

		add_action(
			'load-post-new.php',
			function() {
				$this->maybe_redirect_to_new_order_page();
			}
		);

		add_action(
			'load-post.php',
			function() {
				$this->maybe_redirect_to_edit_order_page();
			}
		);
	}

	/**
	 * If needed, performs a redirection to the main orders page.
	 *
	 * @return void
	 */
	private function maybe_redirect_to_orders_page(): void {
		if ( ! isset( $_GET['post_type'] ) || 'shop_order' !== $_GET['post_type'] ) {
			return;
		}

		// Respect query args, except for 'post_type'.
		$query_args = wp_unslash( $_GET );
		$action     = $query_args['action'] ?? '';
		$posts      = $query_args['post'] ?? array();
		unset( $query_args['post_type'], $query_args['post'], $query_args['_wpnonce'], $query_args['_wp_http_referer'], $query_args['action'] );

		// Remap 'post_status' arg.
		if ( isset( $query_args['post_status'] ) ) {
			$query_args['status'] = $query_args['post_status'];
			unset( $query_args['post_status'] );
		}

		$new_url = $this->page_controller->get_orders_url();
		$new_url = add_query_arg( $query_args, $new_url );

		// Handle bulk actions.
		if ( $action && in_array( $action, array( 'trash', 'untrash', 'delete', 'mark_processing', 'mark_on-hold', 'mark_completed', 'mark_cancelled' ), true ) ) {
			check_admin_referer( 'bulk-posts' );

			$new_url = add_query_arg(
				array(
					'action'           => $action,
					'order'            => $posts,
					'_wp_http_referer' => $this->page_controller->get_orders_url(),
					'_wpnonce'         => wp_create_nonce( 'bulk-orders' ),
				),
				$new_url
			);
		}

		wp_safe_redirect( $new_url, 301 );
		exit;
	}

	/**
	 * If needed, performs a redirection to the new order page.
	 *
	 * @return void
	 */
	private function maybe_redirect_to_new_order_page(): void {
		if ( ! isset( $_GET['post_type'] ) || 'shop_order' !== $_GET['post_type'] ) {
			return;
		}

		// Respect query args, except for 'post_type'.
		$query_args = wp_unslash( $_GET );
		unset( $query_args['post_type'] );

		$new_url = $this->page_controller->get_new_page_url();
		$new_url = add_query_arg( $query_args, $new_url );

		wp_safe_redirect( $new_url, 301 );
		exit;
	}

	/**
	 * If needed, performs a redirection to the edit order page.
	 *
	 * @return void
	 */
	private function maybe_redirect_to_edit_order_page(): void {
		$post_id = absint( $_GET['post'] ?? 0 );

		if ( ! $post_id || ! in_array( get_post_type( $post_id ), array( 'shop_order_placehold', 'shop_order' ), true ) || ! isset( $_GET['action'] ) ) {
			return;
		}

		// Respect query args, except for 'post'.
		$query_args = wp_unslash( $_GET );
		$action     = $query_args['action'];
		unset( $query_args['post'], $query_args['_wpnonce'], $query_args['_wp_http_referer'], $query_args['action'] );

		$new_url = '';

		switch ( $action ) {
			case 'edit':
				$new_url = $this->page_controller->get_edit_url( $post_id );
				break;

			case 'trash':
			case 'untrash':
			case 'delete':
				// Re-generate nonce if validation passes.
				check_admin_referer( $action . '-post_' . $post_id );

				$new_url = add_query_arg(
					array(
						'action'           => $action,
						'order'            => array( $post_id ),
						'_wp_http_referer' => $this->page_controller->get_orders_url(),
						'_wpnonce'         => wp_create_nonce( 'bulk-orders' ),
					),
					$this->page_controller->get_orders_url()
				);

				break;

			default:
				break;
		}

		if ( ! $new_url ) {
			return;
		}

		$new_url = add_query_arg( $query_args, $new_url );

		wp_safe_redirect( $new_url, 301 );
		exit;
	}

}

Internal/Admin/Orders/MetaBoxes/CustomMetaBox.php000064400000032647152333011040015744 0ustar00<?php
/**
 * Meta box to edit and add custom meta values for an order.
 */

namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;

use WC_Data_Store;
use WC_Meta_Data;
use WC_Order;
use WP_Ajax_Response;

/**
 * Class CustomMetaBox.
 */
class CustomMetaBox {

	/**
	 * Update nonce shared among different meta rows.
	 *
	 * @var string
	 */
	private $update_nonce;

	/**
	 * Helper method to get formatted meta data array with proper keys. This can be directly fed to `list_meta()` method.
	 *
	 * @param \WC_Order $order Order object.
	 *
	 * @return array Meta data.
	 */
	private function get_formatted_order_meta_data( \WC_Order $order ) {
		$metadata         = $order->get_meta_data();
		$metadata_to_list = array();
		foreach ( $metadata as $meta ) {
			$data = $meta->get_data();
			if ( is_protected_meta( $data['key'], 'order' ) ) {
				continue;
			}
			$metadata_to_list[] = array(
				'meta_id'    => $data['id'],
				'meta_key'   => $data['key'], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- False positive, not a meta query.
				'meta_value' => maybe_serialize( $data['value'] ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- False positive, not a meta query.
			);
		}
		return $metadata_to_list;
	}

	/**
	 * Renders the meta box to manage custom meta.
	 *
	 * @param \WP_Post|\WC_Order $order_or_post Post or order object that we are rendering for.
	 */
	public function output( $order_or_post ) {
		if ( is_a( $order_or_post, \WP_Post::class ) ) {
			$order = wc_get_order( $order_or_post );
		} else {
			$order = $order_or_post;
		}
		$this->render_custom_meta_form( $this->get_formatted_order_meta_data( $order ), $order );
	}

	/**
	 * Helper method to render layout and actual HTML
	 *
	 * @param array     $metadata_to_list List of metadata to render.
	 * @param \WC_Order $order Order object.
	 */
	private function render_custom_meta_form( array $metadata_to_list, \WC_Order $order ) {
		?>
		<div id="postcustomstuff">
			<div id="ajax-response"></div>
			<?php
			list_meta( $metadata_to_list );
			$this->render_meta_form( $order );
			?>
		</div>
		<p>
			<?php
			printf(
				/* translators: 1: opening documentation tag 2: closing documentation tag. */
				esc_html( __( 'Custom fields can be used to add extra metadata to an order that you can %1$suse in your theme%2$s.', 'woocommerce' ) ),
				'<a href="' . esc_attr__( 'https://wordpress.org/support/article/custom-fields/', 'woocommerce' ) . '">',
				'</a>'
			);
			?>
		</p>
		<?php
	}

	/**
	 * Compute keys to display in autofill when adding new meta key entry in custom meta box.
	 * Currently, returns empty keys, will be implemented after caching is merged.
	 *
	 * @param array|null         $keys Keys to display in autofill.
	 * @param \WP_Post|\WC_Order $order Order object.
	 *
	 * @return array|mixed Array of keys to display in autofill.
	 */
	public function order_meta_keys_autofill( $keys, $order ) {
		if ( is_a( $order, \WC_Order::class ) ) {
			return array();
		}

		return $keys;
	}

	/**
	 * Reimplementation of WP core's `meta_form` function. Renders meta form box.
	 *
	 * @param \WC_Order $order WC_Order object.
	 *
	 * @return void
	 */
	public function render_meta_form( \WC_Order $order ) : void {
		$meta_key_input_id = 'metakeyselect';

		$keys = $this->order_meta_keys_autofill( null, $order );
		/**
		 * Filters values for the meta key dropdown in the Custom Fields meta box.
		 *
		 * Compatibility filter for `postmeta_form_keys` filter.
		 *
		 * @since 6.9.0
		 *
		 * @param array|null $keys Pre-defined meta keys to be used in place of a postmeta query. Default null.
		 * @param \WC_Order  $order The current post object.
		 */
		$keys = apply_filters( 'postmeta_form_keys', $keys, $order );
		?>
		<p><strong><?php esc_html_e( 'Add New Custom Field:', 'woocommerce' ); ?></strong></p>
		<table id="newmeta">
			<thead>
			<tr>
				<th class="left"><label for="<?php echo esc_attr( $meta_key_input_id ); ?>"><?php esc_html_e( 'Name', 'woocommerce' ); ?></label></th>
				<th><label for="metavalue"><?php esc_html_e( 'Value', 'woocommerce' ); ?></label></th>
			</tr>
			</thead>

			<tbody>
			<tr>
				<td id="newmetaleft" class="left">
					<?php if ( $keys ) { ?>
						<select id="metakeyselect" name="metakeyselect">
							<option value="#NONE#"><?php esc_html_e( '&mdash; Select &mdash;', 'woocommerce' ); ?></option>
							<?php
							foreach ( $keys as $key ) {
								if ( is_protected_meta( $key, 'post' ) || ! current_user_can( 'edit_others_shop_order', $order->get_id() ) ) {
									continue;
								}
								echo "\n<option value='" . esc_attr( $key ) . "'>" . esc_html( $key ) . '</option>';
							}
							?>
						</select>
						<input class="hide-if-js" type="text" id="metakeyinput" name="metakeyinput" value="" />
						<a href="#postcustomstuff" class="hide-if-no-js" onclick="jQuery('#metakeyinput, #metakeyselect, #enternew, #cancelnew').toggle();return false;">
							<span id="enternew"><?php esc_html_e( 'Enter new', 'woocommerce' ); ?></span>
							<span id="cancelnew" class="hidden"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></span></a>
					<?php } else { ?>
						<input type="text" id="metakeyinput" name="metakeyinput" value="" />
					<?php } ?>
				</td>
				<td><textarea id="metavalue" name="metavalue" rows="2" cols="25"></textarea></td>
			</tr>

			<tr><td colspan="2">
					<div class="submit">
						<?php
						submit_button(
							__( 'Add Custom Field', 'woocommerce' ),
							'',
							'addmeta',
							false,
							array(
								'id'            => 'newmeta-submit',
								'data-wp-lists' => 'add:the-list:newmeta',
							)
						);
						?>
					</div>
					<?php wp_nonce_field( 'add-meta', '_ajax_nonce-add-meta', false ); ?>
				</td></tr>
			</tbody>
		</table>
		<?php
	}

	/**
	 * Helper method to verify order edit permissions.
	 *
	 * @param int $order_id Order ID.
	 *
	 * @return ?WC_Order WC_Order object if the user can edit the order, die otherwise.
	 */
	private function verify_order_edit_permission_for_ajax( int $order_id ): ?WC_Order {
		if ( ! current_user_can( 'manage_woocommerce' ) || ! current_user_can( 'edit_others_shop_orders' ) ) {
			wp_send_json_error( 'missing_capabilities' );
			wp_die();
		}

		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			wp_send_json_error( 'invalid_order_id' );
			wp_die();
		}
		return $order;
	}

	/**
	 * Reimplementation of WP core's `wp_ajax_add_meta` method to support order custom meta updates with custom tables.
	 */
	public function add_meta_ajax() {
		if ( ! check_ajax_referer( 'add-meta', '_ajax_nonce-add-meta' ) ) {
			wp_send_json_error( 'invalid_nonce' );
			wp_die();
		}

		$order_id = (int) $_POST['order_id'] ?? 0;
		$order    = $this->verify_order_edit_permission_for_ajax( $order_id );

		if ( isset( $_POST['metakeyselect'] ) && '#NONE#' === $_POST['metakeyselect'] && empty( $_POST['metakeyinput'] ) ) {
			wp_die( 1 );
		}

		if ( isset( $_POST['metakeyinput'] ) ) { // add meta.
			$meta_key   = sanitize_text_field( wp_unslash( $_POST['metakeyinput'] ) );
			$meta_value = sanitize_text_field( wp_unslash( $_POST['metavalue'] ?? '' ) );
			$this->handle_add_meta( $order, $meta_key, $meta_value );
		} else { // update.
			$meta = wp_unslash( $_POST['meta'] ?? array() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitization done below in array_walk.
			$this->handle_update_meta( $order, $meta );
		}
	}

	/**
	 * Part of WP Core's `wp_ajax_add_meta`. This is re-implemented to support updating meta for custom tables.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $meta_key Meta key.
	 * @param string   $meta_value Meta value.
	 *
	 * @return void
	 */
	private function handle_add_meta( WC_Order $order, string $meta_key, string $meta_value ) {
		$order_data_store = WC_Data_Store::load( 'order' );
		$count            = 0;
		if ( is_protected_meta( $meta_key ) ) {
			wp_send_json_error( 'protected_meta' );
			wp_die();
		}
		$meta_id  = $order_data_store->add_meta(
			$order,
			new WC_Meta_Data(
				array(
					'key'   => $meta_key,
					'value' => $meta_value,
				)
			)
		);
		$response = new WP_Ajax_Response(
			array(
				'what'     => 'meta',
				'id'       => $meta_id,
				'data'     => $this->list_meta_row(
					array(
						'meta_id'    => $meta_id,
						'meta_key'   => $meta_key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- false positive, not a meta query.
						'meta_value' => $meta_value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query.
					),
					$count
				),
				'position' => 1,
			)
		);
		$response->send();
	}

	/**
	 * Handles updating metadata.
	 *
	 * @param WC_Order $order Order object.
	 * @param array    $meta Meta object to update.
	 *
	 * @return void
	 */
	private function handle_update_meta( WC_Order $order, array $meta ) {
		if ( ! is_array( $meta ) ) {
			wp_send_json_error( 'invalid_meta' );
			wp_die();
		}
		array_walk( $meta, 'sanitize_text_field' );
		$mid = (int) key( $meta );
		if ( ! $mid ) {
			wp_send_json_error( 'invalid_meta_id' );
			wp_die();
		}
		$key   = $meta[ $mid ]['key'];
		$value = $meta[ $mid ]['value'];
		if ( is_protected_meta( $key ) ) {
			wp_send_json_error( 'protected_meta' );
			wp_die();
		}
		if ( '' === trim( $key ) ) {
			wp_send_json_error( 'invalid_meta_key' );
			wp_die();
		}

		$order_data_store = WC_Data_Store::load( 'order' );
		$count            = 0;
		$meta_object      = new WC_Meta_Data(
			array(
				'id'    => $mid,
				'key'   => $key,
				'value' => $value,
			)
		);
		$order_data_store->update_meta( $order, $meta_object );
		$response = new WP_Ajax_Response(
			array(
				'what'     => 'meta',
				'id'       => $mid,
				'old_id'   => $mid,
				'data'     => $this->list_meta_row(
					array(
						'meta_key'   => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- false positive, not a meta query.
						'meta_value' => $value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query.
						'meta_id'    => $mid,
					),
					$count
				),
				'position' => 0,
			)
		);
		$response->send();
	}

	/**
	 * Outputs a single row of public meta data in the Custom Fields meta box.
	 *
	 * @since 2.5.0
	 *
	 * @param array $entry Meta entry.
	 * @param int   $count Sequence number of meta entries.
	 * @return string
	 */
	private function list_meta_row( array $entry, int &$count ) : string {
		if ( is_protected_meta( $entry['meta_key'], 'post' ) ) {
			return '';
		}

		if ( ! $this->update_nonce ) {
			$this->update_nonce = wp_create_nonce( 'add-meta' );
		}

		$r = '';
		++ $count;

		if ( is_serialized( $entry['meta_value'] ) ) {
			if ( is_serialized_string( $entry['meta_value'] ) ) {
				// This is a serialized string, so we should display it.
				$entry['meta_value'] = maybe_unserialize( $entry['meta_value'] ); // // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query.
			} else {
				// This is a serialized array/object so we should NOT display it.
				--$count;
				return '';
			}
		}

		$entry['meta_key']   = esc_attr( $entry['meta_key'] ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- false positive, not a meta query.
		$entry['meta_value'] = esc_textarea( $entry['meta_value'] ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query.
		$entry['meta_id']    = (int) $entry['meta_id'];

		$delete_nonce = wp_create_nonce( 'delete-meta_' . $entry['meta_id'] );

		$r .= "\n\t<tr id='meta-{$entry['meta_id']}'>";
		$r .= "\n\t\t<td class='left'><label class='screen-reader-text' for='meta-{$entry['meta_id']}-key'>" . __( 'Key', 'woocommerce' ) . "</label><input name='meta[{$entry['meta_id']}][key]' id='meta-{$entry['meta_id']}-key' type='text' size='20' value='{$entry['meta_key']}' />";

		$r .= "\n\t\t<div class='submit'>";
		$r .= get_submit_button( __( 'Delete', 'woocommerce' ), 'deletemeta small', "deletemeta[{$entry['meta_id']}]", false, array( 'data-wp-lists' => "delete:the-list:meta-{$entry['meta_id']}::_ajax_nonce:$delete_nonce" ) );
		$r .= "\n\t\t";
		$r .= get_submit_button( __( 'Update', 'woocommerce' ), 'updatemeta small', "meta-{$entry['meta_id']}-submit", false, array( 'data-wp-lists' => "add:the-list:meta-{$entry['meta_id']}::_ajax_nonce-add-meta={$this->update_nonce}" ) );
		$r .= '</div>';
		$r .= wp_nonce_field( 'change-meta', '_ajax_nonce', false, false );
		$r .= '</td>';

		$r .= "\n\t\t<td><label class='screen-reader-text' for='meta-{$entry['meta_id']}-value'>" . __( 'Value', 'woocommerce' ) . "</label><textarea name='meta[{$entry['meta_id']}][value]' id='meta-{$entry['meta_id']}-value' rows='2' cols='30'>{$entry['meta_value']}</textarea></td>\n\t</tr>";
		return $r;
	}

	/**
	 * Reimplementation of WP core's `wp_ajax_delete_meta` method to support order custom meta updates with custom tables.
	 *
	 * @return void
	 */
	public function delete_meta_ajax() {
		$meta_id  = (int) $_POST['id'] ?? 0;
		$order_id = (int) $_POST['order_id'] ?? 0;
		if ( ! $meta_id || ! $order_id ) {
			wp_send_json_error( 'invalid_meta_id' );
			wp_die();
		}
		check_ajax_referer( "delete-meta_$meta_id" );

		$order          = $this->verify_order_edit_permission_for_ajax( $order_id );
		$meta_to_delete = wp_list_filter( $order->get_meta_data(), array( 'id' => $meta_id ) );

		if ( empty( $meta_to_delete ) ) {
			wp_send_json_error( 'invalid_meta_id' );
			wp_die();
		}

		$order->delete_meta_data_by_mid( $meta_id );
		if ( $order->save() ) {
			wp_die( 1 );
		}
		wp_die( 0 );
	}
}
Internal/Admin/Orders/PageController.php000064400000016037152333011040014236 0ustar00<?php
namespace Automattic\WooCommerce\Internal\Admin\Orders;

use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;

/**
 * Controls the different pages/screens associated to the "Orders" menu page.
 */
class PageController {

	use AccessiblePrivateMethods;

	/**
	 * Instance of the posts redirection controller.
	 *
	 * @var PostsRedirectionController
	 */
	private $redirection_controller;

	/**
	 * Instance of the orders list table.
	 *
	 * @var ListTable
	 */
	private $orders_table;

	/**
	 * Instance of orders edit form.
	 *
	 * @var Edit
	 */
	private $order_edit_form;

	/**
	 * Current action.
	 *
	 * @var string
	 */
	private $current_action = '';

	/**
	 * Order object to be used in edit/new form.
	 *
	 * @var \WC_Order
	 */
	private $order;

	/**
	 * Verify that user has permission to edit orders.
	 *
	 * @return void
	 */
	private function verify_edit_permission() {
		if ( 'edit_order' === $this->current_action && ( ! isset( $this->order ) || ! $this->order ) ) {
			wp_die( esc_html__( 'You attempted to edit an order that does not exist. Perhaps it was deleted?', 'woocommerce' ) );
		}
		if ( ! current_user_can( 'edit_others_shop_orders' ) && ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( esc_html__( 'You do not have permission to edit this order', 'woocommerce' ) );
		}
		if ( 'trash' === $this->order->get_status() ) {
			wp_die( esc_html__( 'You cannot edit this item because it is in the Trash. Please restore it and try again.', 'woocommerce' ) );
		}
	}

	/**
	 * Verify that user has permission to create order.
	 *
	 * @return void
	 */
	private function verify_create_permission() {
		if ( ! current_user_can( 'publish_shop_orders' ) && ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( esc_html__( 'You don\'t have permission to create a new order', 'woocommerce' ) );
		}
		if ( isset( $this->order ) ) {
			$this->verify_edit_permission();
		}
	}

	/**
	 * Sets up the page controller, including registering the menu item.
	 *
	 * @return void
	 */
	public function setup(): void {
		$this->redirection_controller = new PostsRedirectionController( $this );

		// Register menu.
		if ( 'admin_menu' === current_action() ) {
			$this->register_menu();
		} else {
			add_action( 'admin_menu', 'register_menu', 9 );
		}

		$this->set_action();

		self::add_action( 'load-woocommerce_page_wc-orders', array( $this, 'handle_load_page_action' ) );
	}

	/**
	 * Perform initialization for the current action.
	 */
	private function handle_load_page_action() {
		if ( method_exists( $this, 'setup_action_' . $this->current_action ) ) {
			$this->{"setup_action_{$this->current_action}"}();
		}
	}

	/**
	 * Sets the current action based on querystring arguments. Defaults to 'list_orders'.
	 *
	 * @return void
	 */
	private function set_action(): void {
		switch ( isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '' ) {
			case 'edit':
				$this->current_action = 'edit_order';
				break;
			case 'new':
				$this->current_action = 'new_order';
				break;
			default:
				$this->current_action = 'list_orders';
				break;
		}
	}

	/**
	 * Registers the "Orders" menu.
	 *
	 * @return void
	 */
	public function register_menu(): void {
		add_submenu_page(
			'woocommerce',
			__( 'Orders', 'woocommerce' ),
			__( 'Orders', 'woocommerce' ),
			'edit_others_shop_orders',
			'wc-orders',
			array( $this, 'output' )
		);

		// In some cases (such as if the authoritative order store was changed earlier in the current request) we
		// need an extra step to remove the menu entry for the menu post type.
		add_action(
			'admin_init',
			function() {
				remove_submenu_page( 'woocommerce', 'edit.php?post_type=shop_order' );
			}
		);
	}

	/**
	 * Outputs content for the current orders screen.
	 *
	 * @return void
	 */
	public function output(): void {
		switch ( $this->current_action ) {
			case 'edit_order':
			case 'new_order':
				if ( ! isset( $this->order_edit_form ) ) {
					$this->order_edit_form = new Edit();
					$this->order_edit_form->setup( $this->order );
				}
				$this->order_edit_form->display();
				break;
			case 'list_orders':
			default:
				$this->orders_table->prepare_items();
				$this->orders_table->display();
				break;
		}
	}

	/**
	 * Handles initialization of the orders list table.
	 *
	 * @return void
	 */
	private function setup_action_list_orders(): void {
		$this->orders_table = wc_get_container()->get( ListTable::class );
		$this->orders_table->setup();
		if ( $this->orders_table->current_action() ) {
			$this->orders_table->handle_bulk_actions();
		}

		$this->strip_http_referer();
	}

	/**
	 * Perform a redirect to remove the `_wp_http_referer` and `_wpnonce` strings if present in the URL (see also
	 * wp-admin/edit.php where a similar process takes place), otherwise the size of this field builds to an
	 * unmanageable length over time.
	 */
	private function strip_http_referer(): void {
		$current_url  = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) );
		$stripped_url = remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), $current_url );

		if ( $stripped_url !== $current_url ) {
			wp_safe_redirect( $stripped_url );
			exit;
		}
	}

	/**
	 * Handles initialization of the orders edit form.
	 *
	 * @return void
	 */
	private function setup_action_edit_order(): void {
		global $theorder;
		$this->order = wc_get_order( absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 ) );
		$this->verify_edit_permission();
		$theorder = $this->order;
	}

	/**
	 * Handles initialization of the orders edit form with a new order.
	 *
	 * @return void
	 */
	private function setup_action_new_order(): void {
		global $theorder;
		$this->verify_create_permission();
		$this->order = new \WC_Order();
		$this->order->set_object_read( false );
		$this->order->set_status( 'auto-draft' );
		$this->order->save();
		$theorder = $this->order;
	}

	/**
	 * Helper method to generate a link to the main orders screen.
	 *
	 * @return string Orders screen URL.
	 */
	public function get_orders_url(): string {
		return wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ?
			admin_url( 'admin.php?page=wc-orders' ) :
			admin_url( 'edit.php?post_type=shop_order' );
	}

	/**
	 * Helper method to generate edit link for an order.
	 *
	 * @param int $order_id Order ID.
	 *
	 * @return string Edit link.
	 */
	public function get_edit_url( int $order_id ) : string {
		return wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ?
			admin_url( 'admin.php?page=wc-orders&id=' . absint( $order_id ) ) . '&action=edit' :
			admin_url( 'post.php?post=' . absint( $order_id ) ) . '&action=edit';
	}

	/**
	 * Helper method to generate a link for creating order.
	 *
	 * @return string
	 */
	public function get_new_page_url() : string {
		return wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ?
			admin_url( 'admin.php?page=wc-orders&action=new' ) :
			admin_url( 'post-new.php?post_type=shop_order' );
	}

}
Internal/Admin/Orders/ListTable.php000064400000116025152333011040013177 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Admin\Orders;

use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use WC_Order;
use WP_List_Table;
use WP_Screen;

/**
 * Admin list table for orders as managed by the OrdersTableDataStore.
 */
class ListTable extends WP_List_Table {
	/**
	 * Contains the arguments to be used in the order query.
	 *
	 * @var array
	 */
	private $order_query_args = array();

	/**
	 * Tracks if a filter (ie, date or customer filter) has been applied.
	 *
	 * @var bool
	 */
	private $has_filter = false;

	/**
	 * Page controller instance for this request.
	 *
	 * @var PageController
	 */
	private $page_controller;

	/**
	 * Tracks whether we're currently inside the trash.
	 *
	 * @var boolean
	 */
	private $is_trash = false;

	/**
	 * Sets up the admin list table for orders (specifically, for orders managed by the OrdersTableDataStore).
	 *
	 * @see WC_Admin_List_Table_Orders for the corresponding class used in relation to the traditional WP Post store.
	 */
	public function __construct() {
		parent::__construct(
			array(
				'singular' => 'order',
				'plural'   => 'orders',
				'ajax'     => false,
			)
		);
	}

	/**
	 * Init method, invoked by DI container.
	 *
	 * @internal This method is not intended to be used directly (except for testing).
	 * @param PageController $page_controller Page controller instance for this request.
	 */
	final public function init( PageController $page_controller ) {
		$this->page_controller = $page_controller;
	}

	/**
	 * Performs setup work required before rendering the table.
	 *
	 * @return void
	 */
	public function setup(): void {
		add_action( 'admin_notices', array( $this, 'bulk_action_notices' ) );
		add_filter( 'manage_woocommerce_page_wc-orders_columns', array( $this, 'get_columns' ), 0 );
		add_filter( 'set_screen_option_edit_orders_per_page', array( $this, 'set_items_per_page' ), 10, 3 );
		add_filter( 'default_hidden_columns', array( $this, 'default_hidden_columns' ), 10, 2 );
		add_action( 'admin_footer', array( $this, 'enqueue_scripts' ) );

		$this->items_per_page();
		set_screen_options();
		add_action( 'manage_' . wc_get_page_screen_id( 'shop-order' ) . '_custom_column', array( $this, 'render_column' ), 10, 2 );
	}

	/**
	 * Render individual column.
	 *
	 * @param string   $column_id Column ID to render.
	 * @param WC_Order $order Order object.
	 */
	public function render_column( $column_id, $order ) {
		if ( ! $order ) {
			return;
		}

		if ( is_callable( array( $this, 'render_' . $column_id . '_column' ) ) ) {
			call_user_func( array( $this, 'render_' . $column_id . '_column' ), $order );
		}
	}

	/**
	 * Handles output for the default column.
	 *
	 * @param \WC_Order $order       Current WooCommerce order object.
	 * @param string    $column_name Identifier for the custom column.
	 */
	public function column_default( $order, $column_name ) {
		/**
		 * Fires for each custom column in the Custom Order Table in the administrative screen.
		 *
		 * @param string    $column_name Identifier for the custom column.
		 * @param \WC_Order $order       Current WooCommerce order object.
		 *
		 * @since 7.0.0
		 */
		do_action( "manage_{$this->screen->id}_custom_column", $column_name, $order );
	}

	/**
	 * Sets up an items-per-page control.
	 */
	private function items_per_page(): void {
		add_screen_option(
			'per_page',
			array(
				'default' => 20,
				'option'  => 'edit_orders_per_page',
			)
		);
	}

	/**
	 * Saves the items-per-page setting.
	 *
	 * @param mixed  $default The default value.
	 * @param string $option  The option being configured.
	 * @param int    $value   The submitted option value.
	 *
	 * @return mixed
	 */
	public function set_items_per_page( $default, string $option, int $value ) {
		return 'edit_orders_per_page' === $option ? absint( $value ) : $default;
	}

	/**
	 * Render the table.
	 *
	 * @return void
	 */
	public function display() {
		$title         = esc_html__( 'Orders', 'woocommerce' );
		$add_new       = esc_html__( 'Add Order', 'woocommerce' );
		$new_page_link = $this->page_controller->get_new_page_url();

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo wp_kses_post(
			"
			<div class='wrap'>
				<h1 class='wp-heading-inline'>{$title}</h1>
				<a href='" . esc_url( $new_page_link ) . "' class='page-title-action'>{$add_new}</a>
				<hr class='wp-header-end'>"
		);

		if ( $this->has_items() || $this->has_filter ) {
			$this->views();

			echo '<form id="wc-orders-filter" method="get" action="' . esc_url( get_admin_url( null, 'admin.php' ) ) . '">';
			$this->print_hidden_form_fields();
			$this->search_box( esc_html__( 'Search orders', 'woocommerce' ), 'orders-search-input' );

			parent::display();
			echo '</form> </div>';
		} else {
			$this->render_blank_state();
		}
	}

	/**
	 * Renders advice in the event that no orders exist yet.
	 *
	 * @return void
	 */
	public function render_blank_state(): void {
		?>
			<div class="woocommerce-BlankState">

				<h2 class="woocommerce-BlankState-message">
					<?php esc_html_e( 'When you receive a new order, it will appear here.', 'woocommerce' ); ?>
				</h2>

				<div class="woocommerce-BlankState-buttons">
					<a class="woocommerce-BlankState-cta button-primary button" target="_blank" href="https://docs.woocommerce.com/document/managing-orders/?utm_source=blankslate&utm_medium=product&utm_content=ordersdoc&utm_campaign=woocommerceplugin"><?php esc_html_e( 'Learn more about orders', 'woocommerce' ); ?></a>
				</div>

			<?php
			/**
			 * Renders after the 'blank state' message for the order list table has rendered.
			 *
			 * @since 6.6.1
			 */
			do_action( 'wc_marketplace_suggestions_orders_empty_state' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment
			?>

			</div>
		<?php
	}

	/**
	 * Retrieves the list of bulk actions available for this table.
	 *
	 * @return array
	 */
	protected function get_bulk_actions() {
		$selected_status = $this->order_query_args['status'] ?? false;

		if ( array( 'trash' ) === $selected_status ) {
			$actions = array(
				'untrash' => __( 'Restore', 'woocommerce' ),
				'delete'  => __( 'Delete permanently', 'woocommerce' ),
			);
		} else {
			$actions = array(
				'mark_processing' => __( 'Change status to processing', 'woocommerce' ),
				'mark_on-hold'    => __( 'Change status to on-hold', 'woocommerce' ),
				'mark_completed'  => __( 'Change status to completed', 'woocommerce' ),
				'mark_cancelled'  => __( 'Change status to cancelled', 'woocommerce' ),
				'trash'           => __( 'Move to Trash', 'woocommerce' ),
			);
		}

		if ( wc_string_to_bool( get_option( 'woocommerce_allow_bulk_remove_personal_data', 'no' ) ) ) {
			$actions['remove_personal_data'] = __( 'Remove personal data', 'woocommerce' );
		}

		return $actions;
	}

	/**
	 * Prepares the list of items for displaying.
	 */
	public function prepare_items() {
		$limit = $this->get_items_per_page( 'edit_orders_per_page' );

		$this->order_query_args = array(
			'limit'    => $limit,
			'page'     => $this->get_pagenum(),
			'paginate' => true,
			'type'     => 'shop_order',
		);

		$this->set_status_args();
		$this->set_order_args();
		$this->set_date_args();
		$this->set_customer_args();
		$this->set_search_args();

		/**
		 * Provides an opportunity to modify the query arguments used in the (Custom Order Table-powered) order list
		 * table.
		 *
		 * @since 6.9.0
		 *
		 * @param array $query_args Arguments to be passed to `wc_get_orders()`.
		 */
		$order_query_args = (array) apply_filters( 'woocommerce_order_list_table_prepare_items_query_args', $this->order_query_args );

		// We must ensure the 'paginate' argument is set.
		$order_query_args['paginate'] = true;

		$orders      = wc_get_orders( $order_query_args );
		$this->items = $orders->orders;

		$max_num_pages = $orders->max_num_pages;

		// Check in case the user has attempted to page beyond the available range of orders.
		if ( 0 === $max_num_pages && $this->order_query_args['page'] > 1 ) {
			$count_query_args          = $order_query_args;
			$count_query_args['page']  = 1;
			$count_query_args['limit'] = 1;
			$order_count               = wc_get_orders( $count_query_args );
			$max_num_pages             = (int) ceil( $order_count->total / $order_query_args['limit'] );
		}

		$this->set_pagination_args(
			array(
				'total_items' => $orders->total ?? 0,
				'per_page'    => $limit,
				'total_pages' => $max_num_pages,
			)
		);

		// Are we inside the trash?
		$this->is_trash = isset( $_REQUEST['status'] ) && 'trash' === $_REQUEST['status'];
	}

	/**
	 * Updates the WC Order Query arguments as needed to support orderable columns.
	 */
	private function set_order_args() {
		$sortable  = $this->get_sortable_columns();
		$field     = sanitize_text_field( wp_unslash( $_GET['orderby'] ?? '' ) );
		$direction = strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ?? '' ) ) );

		if ( ! in_array( $field, $sortable, true ) ) {
			return;
		}

		$this->order_query_args['orderby'] = $field;
		$this->order_query_args['order']   = in_array( $direction, array( 'ASC', 'DESC' ), true ) ? $direction : 'ASC';
	}

	/**
	 * Implements date (month-based) filtering.
	 */
	private function set_date_args() {
		$year_month = sanitize_text_field( wp_unslash( $_GET['m'] ?? '' ) );

		if ( empty( $year_month ) || ! preg_match( '/^[0-9]{6}$/', $year_month ) ) {
			return;
		}

		$year  = (int) substr( $year_month, 0, 4 );
		$month = (int) substr( $year_month, 4, 2 );

		if ( $month < 0 || $month > 12 ) {
			return;
		}

		$last_day_of_month                      = date_create( "$year-$month" )->format( 'Y-m-t' );
		$this->order_query_args['date_created'] = "$year-$month-01..." . $last_day_of_month;
		$this->has_filter                       = true;
	}

	/**
	 * Implements filtering of orders by customer.
	 */
	private function set_customer_args() {
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$customer = (int) wp_unslash( $_GET['_customer_user'] ?? '' );

		if ( $customer < 1 ) {
			return;
		}

		$this->order_query_args['customer'] = $customer;
		$this->has_filter                   = true;
	}

	/**
	 * Implements filtering of orders by status.
	 */
	private function set_status_args() {
		$status         = trim( sanitize_text_field( wp_unslash( $_REQUEST['status'] ?? '' ) ) );
		$query_statuses = array();

		if ( empty( $status ) || 'all' === $status ) {
			$query_statuses = array_intersect(
				array_keys( wc_get_order_statuses() ),
				get_post_stati( array( 'show_in_admin_all_list' => true ), 'names' )
			);
		} else {
			$query_statuses[] = $status;
			$this->has_filter = true;
		}

		$this->order_query_args['status'] = $query_statuses;
	}

	/**
	 * Implements order search.
	 */
	private function set_search_args(): void {
		$search_term = trim( sanitize_text_field( wp_unslash( $_REQUEST['s'] ?? '' ) ) );

		if ( ! empty( $search_term ) ) {
			$this->order_query_args['s'] = $search_term;
			$this->has_filter            = true;
		}
	}

	/**
	 * Get the list of views for this table (all orders, completed orders, etc, each with a count of the number of
	 * corresponding orders).
	 *
	 * @return array
	 */
	public function get_views() {
		$view_counts = array();
		$view_links  = array();
		$statuses    = wc_get_order_statuses();
		$current     = isset( $_GET['status'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['status'] ?? '' ) ) : 'all';

		// Add 'draft' and 'trash' to list.
		foreach ( array( 'draft', 'trash' ) as $wp_status ) {
			$statuses[ $wp_status ] = ( get_post_status_object( $wp_status ) )->label;
		}

		$statuses_in_list = array_intersect( array_keys( $statuses ), get_post_stati( array( 'show_in_admin_status_list' => true ) ) );

		foreach ( $statuses_in_list as $slug ) {
			$total_in_status = $this->count_orders_by_status( $slug );

			if ( $total_in_status > 0 ) {
				$view_counts[ $slug ] = $total_in_status;
			}
		}

		$all_count         = array_sum( $view_counts );
		$view_links['all'] = $this->get_view_link( 'all', __( 'All', 'woocommerce' ), $all_count, '' === $current || 'all' === $current );

		foreach ( $view_counts as $slug => $count ) {
			$view_links[ $slug ] = $this->get_view_link( $slug, $statuses[ $slug ], $count, $slug === $current );
		}

		return $view_links;
	}

	/**
	 * Count orders by status.
	 *
	 * @param string $status The order status we are interested in.
	 *
	 * @return int
	 */
	private function count_orders_by_status( string $status ): int {
		$orders = wc_get_orders(
			array(
				'limit'  => -1,
				'return' => 'ids',
				'status' => $status,
			)
		);

		return count( $orders );
	}

	/**
	 * Form a link to use in the list of table views.
	 *
	 * @param string $slug    Slug used to identify the view (usually the order status slug).
	 * @param string $name    Human-readable name of the view (usually the order status label).
	 * @param int    $count   Number of items in this view.
	 * @param bool   $current If this is the current view.
	 *
	 * @return string
	 */
	private function get_view_link( string $slug, string $name, int $count, bool $current ): string {
		$url   = esc_url( add_query_arg( 'status', $slug, get_admin_url( null, 'admin.php?page=wc-orders' ) ) );
		$name  = esc_html( $name );
		$count = absint( $count );
		$class = $current ? 'class="current"' : '';

		return "<a href='$url' $class>$name <span class='count'>($count)</span></a>";
	}

	/**
	 * Extra controls to be displayed between bulk actions and pagination.
	 *
	 * @param string $which Either 'top' or 'bottom'.
	 */
	protected function extra_tablenav( $which ) {
		echo '<div class="alignleft actions">';

		if ( 'top' === $which ) {
			$this->months_filter();
			$this->customers_filter();

			submit_button( __( 'Filter', 'woocommerce' ), '', 'filter_action', false, array( 'id' => 'order-query-submit' ) );
		}

		if ( $this->is_trash && $this->has_items() && current_user_can( 'edit_others_shop_orders' ) ) {
			submit_button( __( 'Empty Trash', 'woocommerce' ), 'apply', 'delete_all', false );
		}

		echo '</div>';
	}

	/**
	 * Render the months filter dropdown.
	 *
	 * @return void
	 */
	private function months_filter() {
		// XXX: [review] we may prefer to move this logic outside of the ListTable class.

		global $wp_locale;
		global $wpdb;

		$orders_table = esc_sql( OrdersTableDataStore::get_orders_table_name() );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$order_dates = $wpdb->get_results(
			"
				SELECT DISTINCT YEAR( date_created_gmt ) AS year,
								MONTH( date_created_gmt ) AS month

				FROM $orders_table

				WHERE status NOT IN (
					'trash'
				)

				ORDER BY year DESC, month DESC;
			"
		);

		$m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0;
		echo '<select name="m" id="filter-by-date">';
		echo '<option ' . selected( $m, 0, false ) . ' value="0">' . esc_html__( 'All dates', 'woocommerce' ) . '</option>';

		foreach ( $order_dates as $date ) {
			$month           = zeroise( $date->month, 2 );
			$month_year_text = sprintf(
				/* translators: 1: Month name, 2: 4-digit year. */
				esc_html_x( '%1$s %2$d', 'order dates dropdown', 'woocommerce' ),
				$wp_locale->get_month( $month ),
				$date->year
			);

			printf(
				'<option %1$s value="%2$s">%3$s</option>\n',
				selected( $m, $date->year . $month, false ),
				esc_attr( $date->year . $month ),
				esc_html( $month_year_text )
			);
		}

		echo '</select>';
	}

	/**
	 * Render the customer filter dropdown.
	 *
	 * @return void
	 */
	public function customers_filter() {
		$user_string = '';
		$user_id     = '';

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( ! empty( $_GET['_customer_user'] ) ) {
			$user_id = absint( $_GET['_customer_user'] );
			$user    = get_user_by( 'id', $user_id );

			$user_string = sprintf(
				/* translators: 1: user display name 2: user ID 3: user email */
				esc_html__( '%1$s (#%2$s &ndash; %3$s)', 'woocommerce' ),
				$user->display_name,
				absint( $user->ID ),
				$user->user_email
			);
		}

		// Note: use of htmlspecialchars (below) is to prevent XSS when rendered by selectWoo.
		?>
		<select class="wc-customer-search" name="_customer_user" data-placeholder="<?php esc_attr_e( 'Filter by registered customer', 'woocommerce' ); ?>" data-allow_clear="true">
			<option value="<?php echo esc_attr( $user_id ); ?>" selected="selected"><?php echo htmlspecialchars( wp_kses_post( $user_string ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></option>
		</select>
		<?php
	}

	/**
	 * Get list columns.
	 *
	 * @return array
	 */
	public function get_columns() {
		return array(
			'cb'               => '<input type="checkbox" />',
			'order_number'     => esc_html__( 'Order', 'woocommerce' ),
			'order_date'       => esc_html__( 'Date', 'woocommerce' ),
			'order_status'     => esc_html__( 'Status', 'woocommerce' ),
			'billing_address'  => esc_html__( 'Billing', 'woocommerce' ),
			'shipping_address' => esc_html__( 'Ship to', 'woocommerce' ),
			'order_total'      => esc_html__( 'Total', 'woocommerce' ),
			'wc_actions'       => esc_html__( 'Actions', 'woocommerce' ),
		);
	}

	/**
	 * Defines the default sortable columns.
	 *
	 * @return string[]
	 */
	public function get_sortable_columns() {
		return array(
			'order_number' => 'ID',
			'order_date'   => 'date',
			'order_total'  => 'order_total',
		);
	}

	/**
	 * Specify the columns we wish to hide by default.
	 *
	 * @param array     $hidden Columns set to be hidden.
	 * @param WP_Screen $screen Screen object.
	 *
	 * @return array
	 */
	public function default_hidden_columns( array $hidden, WP_Screen $screen ) {
		if ( isset( $screen->id ) && wc_get_page_screen_id( 'shop-order' ) === $screen->id ) {
			$hidden = array_merge(
				$hidden,
				array(
					'billing_address',
					'shipping_address',
					'wc_actions',
				)
			);
		}

		return $hidden;
	}

	/**
	 * Checklist column, used for selecting items for processing by a bulk action.
	 *
	 * @param WC_Order $item The order object for the current row.
	 *
	 * @return string
	 */
	public function column_cb( $item ) {
		return sprintf( '<input type="checkbox" name="%1$s[]" value="%2$s" />', esc_attr( $this->_args['singular'] ), esc_attr( $item->get_id() ) );
	}

	/**
	 * Renders the order number, customer name and provides a preview link.
	 *
	 * @param WC_Order $order The order object for the current row.
	 *
	 * @return void
	 */
	public function render_order_number_column( WC_Order $order ): void {
		$buyer = '';

		if ( $order->get_billing_first_name() || $order->get_billing_last_name() ) {
			/* translators: 1: first name 2: last name */
			$buyer = trim( sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $order->get_billing_first_name(), $order->get_billing_last_name() ) );
		} elseif ( $order->get_billing_company() ) {
			$buyer = trim( $order->get_billing_company() );
		} elseif ( $order->get_customer_id() ) {
			$user  = get_user_by( 'id', $order->get_customer_id() );
			$buyer = ucwords( $user->display_name );
		}

		/**
		 * Filter buyer name in list table orders.
		 *
		 * @since 3.7.0
		 *
		 * @param string   $buyer Buyer name.
		 * @param WC_Order $order Order data.
		 */
		$buyer = apply_filters( 'woocommerce_admin_order_buyer_name', $buyer, $order );

		if ( $order->get_status() === 'trash' ) {
			echo '<strong>#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . '</strong>';
		} else {
			echo '<a href="#" class="order-preview" data-order-id="' . absint( $order->get_id() ) . '" title="' . esc_attr( __( 'Preview', 'woocommerce' ) ) . '">' . esc_html( __( 'Preview', 'woocommerce' ) ) . '</a>';
			echo '<a href="' . esc_url( $this->get_order_edit_link( $order ) ) . '" class="order-view"><strong>#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . '</strong></a>';
		}
	}

	/**
	 * Get the edit link for an order.
	 *
	 * @param WC_Order $order Order object.
	 *
	 * @return string Edit link for the order.
	 */
	private function get_order_edit_link( WC_Order $order ) : string {
		return $this->page_controller->get_edit_url( $order->get_id() );
	}

	/**
	 * Renders the order date.
	 *
	 * @param WC_Order $order The order object for the current row.
	 *
	 * @return void
	 */
	public function render_order_date_column( WC_Order $order ): void {
		$order_timestamp = $order->get_date_created() ? $order->get_date_created()->getTimestamp() : '';

		if ( ! $order_timestamp ) {
			echo '&ndash;';
			return;
		}

		// Check if the order was created within the last 24 hours, and not in the future.
		if ( $order_timestamp > strtotime( '-1 day', time() ) && $order_timestamp <= time() ) {
			$show_date = sprintf(
			/* translators: %s: human-readable time difference */
				_x( '%s ago', '%s = human-readable time difference', 'woocommerce' ),
				human_time_diff( $order->get_date_created()->getTimestamp(), time() )
			);
		} else {
			$show_date = $order->get_date_created()->date_i18n( apply_filters( 'woocommerce_admin_order_date_format', __( 'M j, Y', 'woocommerce' ) ) ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
		}
		printf(
			'<time datetime="%1$s" title="%2$s">%3$s</time>',
			esc_attr( $order->get_date_created()->date( 'c' ) ),
			esc_html( $order->get_date_created()->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ),
			esc_html( $show_date )
		);
	}

	/**
	 * Renders the order status.
	 *
	 * @param WC_Order $order The order object for the current row.
	 *
	 * @return void
	 */
	public function render_order_status_column( WC_Order $order ): void {
		$tooltip                 = '';
		$comment_count           = get_comment_count( $order->get_id() );
		$approved_comments_count = absint( $comment_count['approved'] );

		if ( $approved_comments_count ) {
			$latest_notes = wc_get_order_notes(
				array(
					'order_id' => $order->get_id(),
					'limit'    => 1,
					'orderby'  => 'date_created_gmt',
				)
			);

			$latest_note = current( $latest_notes );

			if ( isset( $latest_note->content ) && 1 === $approved_comments_count ) {
				$tooltip = wc_sanitize_tooltip( $latest_note->content );
			} elseif ( isset( $latest_note->content ) ) {
				/* translators: %d: notes count */
				$tooltip = wc_sanitize_tooltip( $latest_note->content . '<br/><small style="display:block">' . sprintf( _n( 'Plus %d other note', 'Plus %d other notes', ( $approved_comments_count - 1 ), 'woocommerce' ), $approved_comments_count - 1 ) . '</small>' );
			} else {
				/* translators: %d: notes count */
				$tooltip = wc_sanitize_tooltip( sprintf( _n( '%d note', '%d notes', $approved_comments_count, 'woocommerce' ), $approved_comments_count ) );
			}
		}

		// Gracefully handle legacy statuses.
		if ( in_array( $order->get_status(), array( 'trash', 'draft' ), true ) ) {
			$status_name = ( get_post_status_object( $order->get_status() ) )->label;
		} else {
			$status_name = wc_get_order_status_name( $order->get_status() );
		}

		if ( $tooltip ) {
			printf( '<mark class="order-status %s tips" data-tip="%s"><span>%s</span></mark>', esc_attr( sanitize_html_class( 'status-' . $order->get_status() ) ), wp_kses_post( $tooltip ), esc_html( $status_name ) );
		} else {
			printf( '<mark class="order-status %s"><span>%s</span></mark>', esc_attr( sanitize_html_class( 'status-' . $order->get_status() ) ), esc_html( $status_name ) );
		}
	}

	/**
	 * Renders order billing information.
	 *
	 * @param WC_Order $order The order object for the current row.
	 *
	 * @return void
	 */
	public function render_billing_address_column( WC_Order $order ): void {
		$address = $order->get_formatted_billing_address();

		if ( $address ) {
			echo esc_html( preg_replace( '#<br\s*/?>#i', ', ', $address ) );

			if ( $order->get_payment_method() ) {
				/* translators: %s: payment method */
				echo '<span class="description">' . sprintf( esc_html__( 'via %s', 'woocommerce' ), esc_html( $order->get_payment_method_title() ) ) . '</span>';
			}
		} else {
			echo '&ndash;';
		}
	}

	/**
	 * Renders order shipping information.
	 *
	 * @param WC_Order $order The order object for the current row.
	 *
	 * @return void
	 */
	public function render_shipping_address_column( WC_Order $order ): void {
		$address = $order->get_formatted_shipping_address();

		if ( $address ) {
			echo '<a target="_blank" href="' . esc_url( $order->get_shipping_address_map_url() ) . '">' . esc_html( preg_replace( '#<br\s*/?>#i', ', ', $address ) ) . '</a>';
			if ( $order->get_shipping_method() ) {
				/* translators: %s: shipping method */
				echo '<span class="description">' . sprintf( esc_html__( 'via %s', 'woocommerce' ), esc_html( $order->get_shipping_method() ) ) . '</span>';
			}
		} else {
			echo '&ndash;';
		}
	}

	/**
	 * Renders the order total.
	 *
	 * @param WC_Order $order The order object for the current row.
	 *
	 * @return void
	 */
	public function render_order_total_column( WC_Order $order ): void {
		if ( $order->get_payment_method_title() ) {
			/* translators: %s: method */
			echo '<span class="tips" data-tip="' . esc_attr( sprintf( __( 'via %s', 'woocommerce' ), $order->get_payment_method_title() ) ) . '">' . wp_kses_post( $order->get_formatted_order_total() ) . '</span>';
		} else {
			echo wp_kses_post( $order->get_formatted_order_total() );
		}
	}

	/**
	 * Renders order actions.
	 *
	 * @param WC_Order $order The order object for the current row.
	 *
	 * @return void
	 */
	public function render_wc_actions_column( WC_Order $order ): void {
		echo '<p>';

		/**
		 * Fires before the order action buttons (within the actions column for the order list table)
		 * are registered.
		 *
		 * @param WC_Order $order Current order object.
		 * @since 6.7.0
		 */
		do_action( 'woocommerce_admin_order_actions_start', $order );

		$actions = array();

		if ( $order->has_status( array( 'pending', 'on-hold' ) ) ) {
			$actions['processing'] = array(
				'url'    => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=processing&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ),
				'name'   => __( 'Processing', 'woocommerce' ),
				'action' => 'processing',
			);
		}

		if ( $order->has_status( array( 'pending', 'on-hold', 'processing' ) ) ) {
			$actions['complete'] = array(
				'url'    => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=completed&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ),
				'name'   => __( 'Complete', 'woocommerce' ),
				'action' => 'complete',
			);
		}

		/**
		 * Provides an opportunity to modify the action buttons within the order list table.
		 *
		 * @param array    $action Order actions.
		 * @param WC_Order $order  Current order object.
		 * @since 6.7.0
		 */
		$actions = apply_filters( 'woocommerce_admin_order_actions', $actions, $order );

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo wc_render_action_buttons( $actions );

		/**
		 * Fires after the order action buttons (within the actions column for the order list table)
		 * are rendered.
		 *
		 * @param WC_Order $order Current order object.
		 * @since 6.7.0
		 */
		do_action( 'woocommerce_admin_order_actions_end', $order );

		echo '</p>';
	}

	/**
	 * Outputs hidden fields used to retain state when filtering.
	 *
	 * @return void
	 */
	private function print_hidden_form_fields(): void {
		echo '<input type="hidden" name="page" value="wc-orders" >';

		$state_params = array(
			'paged',
			'status',
		);

		foreach ( $state_params as $param ) {
			if ( ! isset( $_GET[ $param ] ) ) {
				continue;
			}

			echo '<input type="hidden" name="' . esc_attr( $param ) . '" value="' . esc_attr( sanitize_text_field( wp_unslash( $_GET[ $param ] ) ) ) . '" >';
		}
	}

	/**
	 * Gets the current action selected from the bulk actions dropdown.
	 *
	 * @return string|false The action name. False if no action was selected.
	 */
	public function current_action() {
		if ( ! empty( $_REQUEST['delete_all'] ) ) {
			return 'delete_all';
		}

		return parent::current_action();
	}

	/**
	 * Handle bulk actions.
	 */
	public function handle_bulk_actions() {
		$action = $this->current_action();

		if ( ! $action ) {
			return;
		}

		check_admin_referer( 'bulk-orders' );

		$redirect_to = remove_query_arg( array( 'deleted', 'ids' ), wp_get_referer() );
		$redirect_to = add_query_arg( 'paged', $this->get_pagenum(), $redirect_to );

		if ( 'delete_all' === $action ) {
			// Get all trashed orders.
			$ids = wc_get_orders(
				array(
					'type'   => 'shop_order',
					'status' => 'trash',
					'limit'  => -1,
					'return' => 'ids',
				)
			);

			$action = 'delete';
		} else {
			$ids = isset( $_REQUEST['order'] ) ? array_reverse( array_map( 'absint', $_REQUEST['order'] ) ) : array();
		}

		/**
		 * Allows 3rd parties to modify order IDs about to be affected by a bulk action.
		 *
		 * @param array Array of order IDs.
		 */
		$ids = apply_filters( // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment
			'woocommerce_bulk_action_ids',
			$ids,
			$action,
			'order'
		);

		if ( ! $ids ) {
			wp_safe_redirect( $redirect_to );
			exit;
		}

		$report_action = '';
		$changed       = 0;

		if ( 'remove_personal_data' === $action ) {
			$report_action = 'removed_personal_data';
			$changed       = $this->do_bulk_action_remove_personal_data( $ids );
		} elseif ( 'trash' === $action ) {
			$changed       = $this->do_delete( $ids );
			$report_action = 'trashed';
		} elseif ( 'delete' === $action ) {
			$changed       = $this->do_delete( $ids, true );
			$report_action = 'deleted';
		} elseif ( 'untrash' === $action ) {
			$changed       = $this->do_untrash( $ids );
			$report_action = 'untrashed';
		} elseif ( false !== strpos( $action, 'mark_' ) ) {
			$order_statuses = wc_get_order_statuses();
			$new_status     = substr( $action, 5 );
			$report_action  = 'marked_' . $new_status;

			if ( isset( $order_statuses[ 'wc-' . $new_status ] ) ) {
				$changed = $this->do_bulk_action_mark_orders( $ids, $new_status );
			}
		} else {
			$screen = get_current_screen()->id;

			/**
			 * This action is documented in /wp-admin/edit.php (it is a core WordPress hook).
			 *
			 * @since 7.2.0
			 *
			 * @param string $redirect_to The URL to redirect to after processing the bulk actions.
			 * @param string $action      The current bulk action.
			 * @param int[]  $ids         IDs for the orders to be processed.
			 */
			$custom_sendback = apply_filters( "handle_bulk_actions-{$screen}", $redirect_to, $action, $ids ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
		}

		if ( ! empty( $custom_sendback ) ) {
			$redirect_to = $custom_sendback;
		} elseif ( $changed ) {
			$redirect_to = add_query_arg(
				array(
					'bulk_action' => $report_action,
					'changed'     => $changed,
					'ids'         => implode( ',', $ids ),
				),
				$redirect_to
			);
		}

		wp_safe_redirect( $redirect_to );
		exit;
	}

	/**
	 * Implements the "remove personal data" bulk action.
	 *
	 * @param array $order_ids The Order IDs.
	 * @return int Number of orders modified.
	 */
	private function do_bulk_action_remove_personal_data( $order_ids ): int {
		$changed = 0;

		foreach ( $order_ids as $id ) {
			$order = wc_get_order( $id );

			if ( ! $order ) {
				continue;
			}

			do_action( 'woocommerce_remove_order_personal_data', $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
			$changed++;
		}

		return $changed;
	}

	/**
	 * Implements the "mark <status>" bulk action.
	 *
	 * @param array  $order_ids  The order IDs to change.
	 * @param string $new_status The new order status.
	 * @return int Number of orders modified.
	 */
	private function do_bulk_action_mark_orders( $order_ids, $new_status ): int {
		$changed = 0;

		// Initialize payment gateways in case order has hooked status transition actions.
		WC()->payment_gateways();

		foreach ( $order_ids as $id ) {
			$order = wc_get_order( $id );

			if ( ! $order ) {
				continue;
			}

			$order->update_status( $new_status, __( 'Order status changed by bulk edit.', 'woocommerce' ), true );
			do_action( 'woocommerce_order_edit_status', $id, $new_status ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
			$changed++;
		}

		return $changed;
	}

	/**
	 * Handles bulk trashing of orders.
	 *
	 * @param int[] $ids Order IDs to be trashed.
	 * @param bool  $force_delete When set, the order will be completed deleted. Otherwise, it will be trashed.
	 *
	 * @return int Number of orders that were trashed.
	 */
	private function do_delete( array $ids, bool $force_delete = false ): int {
		$orders_store = wc_get_container()->get( OrdersTableDataStore::class );
		$delete_args  = $force_delete ? array( 'force_delete' => true ) : array();
		$changed      = 0;

		foreach ( $ids as $id ) {
			$order = wc_get_order( $id );
			$orders_store->delete( $order, $delete_args );
			$updated_order = wc_get_order( $id );

			if ( ( $force_delete && false === $updated_order ) || ( ! $force_delete && $updated_order->get_status() === 'trash' ) ) {
				$changed++;
			}
		}

		return $changed;
	}

	/**
	 * Handles bulk restoration of trashed orders.
	 *
	 * @param array $ids Order IDs to be restored to their previous status.
	 *
	 * @return int Number of orders that were restored from the trash.
	 */
	private function do_untrash( array $ids ): int {
		$orders_store = wc_get_container()->get( OrdersTableDataStore::class );
		$changed      = 0;

		foreach ( $ids as $id ) {
			if ( $orders_store->untrash_order( wc_get_order( $id ) ) ) {
				$changed++;
			}
		}

		return $changed;
	}

	/**
	 * Show confirmation message that order status changed for number of orders.
	 */
	public function bulk_action_notices() {
		if ( empty( $_REQUEST['bulk_action'] ) ) {
			return;
		}

		$order_statuses = wc_get_order_statuses();
		$number         = absint( $_REQUEST['changed'] ?? 0 );
		$bulk_action    = wc_clean( wp_unslash( $_REQUEST['bulk_action'] ) );
		$message        = '';

		// Check if any status changes happened.
		foreach ( $order_statuses as $slug => $name ) {
			if ( 'marked_' . str_replace( 'wc-', '', $slug ) === $bulk_action ) { // WPCS: input var ok, CSRF ok.
				/* translators: %s: orders count */
				$message = sprintf( _n( '%s order status changed.', '%s order statuses changed.', $number, 'woocommerce' ), number_format_i18n( $number ) );
				break;
			}
		}

		switch ( $bulk_action ) {
			case 'removed_personal_data':
				/* translators: %s: orders count */
				$message = sprintf( _n( 'Removed personal data from %s order.', 'Removed personal data from %s orders.', $number, 'woocommerce' ), number_format_i18n( $number ) );
				echo '<div class="updated"><p>' . esc_html( $message ) . '</p></div>';
				break;

			case 'trashed':
				/* translators: %s: orders count */
				$message = sprintf( _n( '%s order moved to the Trash.', '%s orders moved to the Trash.', $number, 'woocommerce' ), number_format_i18n( $number ) );
				break;

			case 'untrashed':
				/* translators: %s: orders count */
				$message = sprintf( _n( '%s order restored from the Trash.', '%s orders restored from the Trash.', $number, 'woocommerce' ), number_format_i18n( $number ) );
				break;

			case 'deleted':
				/* translators: %s: orders count */
				$message = sprintf( _n( '%s order permanently deleted.', '%s orders permanently deleted.', $number, 'woocommerce' ), number_format_i18n( $number ) );
				break;
		}

		if ( ! empty( $message ) ) {
			echo '<div class="updated"><p>' . esc_html( $message ) . '</p></div>';
		}
	}

	/**
	 * Enqueue list table scripts.
	 *
	 * @return void
	 */
	public function enqueue_scripts(): void {
		echo $this->get_order_preview_template(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		wp_enqueue_script( 'wc-orders' );
	}

	/**
	 * Returns the HTML for the order preview template.
	 *
	 * @return string HTML template.
	 */
	public function get_order_preview_template(): string {
		$order_edit_url_placeholder =
			wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled()
			? esc_url( admin_url( 'admin.php?page=wc-orders&action=edit' ) ) . '&id={{ data.data.id }}'
			: esc_url( admin_url( 'post.php?action=edit' ) ) . '&post={{ data.data.id }}';

		ob_start();
		?>
		<script type="text/template" id="tmpl-wc-modal-view-order">
			<div class="wc-backbone-modal wc-order-preview">
				<div class="wc-backbone-modal-content">
					<section class="wc-backbone-modal-main" role="main">
						<header class="wc-backbone-modal-header">
							<mark class="order-status status-{{ data.status }}"><span>{{ data.status_name }}</span></mark>
							<?php /* translators: %s: order ID */ ?>
							<h1><?php echo esc_html( sprintf( __( 'Order #%s', 'woocommerce' ), '{{ data.order_number }}' ) ); ?></h1>
							<button class="modal-close modal-close-link dashicons dashicons-no-alt">
								<span class="screen-reader-text"><?php esc_html_e( 'Close modal panel', 'woocommerce' ); ?></span>
							</button>
						</header>
						<article>
							<?php do_action( 'woocommerce_admin_order_preview_start' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment ?>

							<div class="wc-order-preview-addresses">
								<div class="wc-order-preview-address">
									<h2><?php esc_html_e( 'Billing details', 'woocommerce' ); ?></h2>
									{{{ data.formatted_billing_address }}}

									<# if ( data.data.billing.email ) { #>
										<strong><?php esc_html_e( 'Email', 'woocommerce' ); ?></strong>
										<a href="mailto:{{ data.data.billing.email }}">{{ data.data.billing.email }}</a>
									<# } #>

									<# if ( data.data.billing.phone ) { #>
										<strong><?php esc_html_e( 'Phone', 'woocommerce' ); ?></strong>
										<a href="tel:{{ data.data.billing.phone }}">{{ data.data.billing.phone }}</a>
									<# } #>

									<# if ( data.payment_via ) { #>
										<strong><?php esc_html_e( 'Payment via', 'woocommerce' ); ?></strong>
										{{{ data.payment_via }}}
									<# } #>
								</div>
								<# if ( data.needs_shipping ) { #>
									<div class="wc-order-preview-address">
										<h2><?php esc_html_e( 'Shipping details', 'woocommerce' ); ?></h2>
										<# if ( data.ship_to_billing ) { #>
											{{{ data.formatted_billing_address }}}
										<# } else { #>
											<a href="{{ data.shipping_address_map_url }}" target="_blank">{{{ data.formatted_shipping_address }}}</a>
										<# } #>

										<# if ( data.shipping_via ) { #>
											<strong><?php esc_html_e( 'Shipping method', 'woocommerce' ); ?></strong>
											{{ data.shipping_via }}
										<# } #>
									</div>
								<# } #>

								<# if ( data.data.customer_note ) { #>
									<div class="wc-order-preview-note">
										<strong><?php esc_html_e( 'Note', 'woocommerce' ); ?></strong>
										{{ data.data.customer_note }}
									</div>
								<# } #>
							</div>

							{{{ data.item_html }}}

							<?php do_action( 'woocommerce_admin_order_preview_end' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment ?>
						</article>
						<footer>
							<div class="inner">
								{{{ data.actions_html }}}

								<a class="button button-primary button-large" aria-label="<?php esc_attr_e( 'Edit this order', 'woocommerce' ); ?>" href="<?php echo $order_edit_url_placeholder; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a>
							</div>
						</footer>
					</section>
				</div>
			</div>
			<div class="wc-backbone-modal-backdrop modal-close"></div>
		</script>
		<?php

		$html = ob_get_clean();

		return $html;
	}

}
Internal/Admin/Orders/COTRedirectionController.php000064400000005424152333011040016175 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Admin\Orders;

use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;

/**
 * When Custom Order Tables are not the default order store (ie, posts are authoritative), we should take care of
 * redirecting requests for the order editor and order admin list table to the equivalent posts-table screens.
 *
 * If the redirect logic is problematic, it can be unhooked using code like the following example:
 *
 *     remove_action(
 *         'admin_page_access_denied',
 *         array( wc_get_container()->get( COTRedirectionController::class ), 'handle_hpos_admin_requests' )
 *     );
 */
class COTRedirectionController {
	use AccessiblePrivateMethods;

	/**
	 * Add hooks needed to perform our magic.
	 */
	public function setup(): void {
		// Only take action in cases where access to the admin screen would otherwise be denied.
		self::add_action( 'admin_page_access_denied', array( $this, 'handle_hpos_admin_requests' ) );
	}

	/**
	 * Listen for denied admin requests and, if they appear to relate to HPOS admin screens, potentially
	 * redirect the user to the equivalent CPT-driven screens.
	 *
	 * @param array|null $query_params The query parameters to use when determining the redirect. If not provided, the $_GET superglobal will be used.
	 */
	private function handle_hpos_admin_requests( $query_params = null ) {
		$query_params = is_array( $query_params ) ? $query_params : $_GET;

		if ( ! isset( $query_params['page'] ) || 'wc-orders' !== $query_params['page'] ) {
			return;
		}

		$params = wp_unslash( $query_params );
		$action = $params['action'] ?? '';
		unset( $params['page'] );

		if ( 'edit' === $action && isset( $params['id'] ) ) {
			$params['post'] = $params['id'];
			unset( $params['id'] );
			$new_url = add_query_arg( $params, get_admin_url( null, 'post.php' ) );
		} elseif ( 'new' === $action ) {
			unset( $params['action'] );
			$params['post_type'] = 'shop_order';
			$new_url             = add_query_arg( $params, get_admin_url( null, 'post-new.php' ) );
		} else {
			// If nonce parameters are present and valid, rebuild them for the CPT admin list table.
			if ( isset( $params['_wpnonce'] ) && check_admin_referer( 'bulk-orders' ) ) {
				$params['_wp_http_referer'] = get_admin_url( null, 'edit.php?post_type=shop_order' );
				$params['_wpnonce']         = wp_create_nonce( 'bulk-posts' );
			}

			// If an `order` array parameter is present, rename as `post`.
			if ( isset( $params['order'] ) && is_array( $params['order'] ) ) {
				$params['post'] = $params['order'];
				unset( $params['order'] );
			}

			$params['post_type'] = 'shop_order';
			$new_url             = add_query_arg( $params, get_admin_url( null, 'edit.php' ) );
		}

		if ( ! empty( $new_url ) && wp_safe_redirect( $new_url, 301 ) ) {
			exit;
		}
	}
}
Internal/Admin/Orders/Edit.php000064400000020150152333011040012172 0ustar00<?php
/**
 * Renders order edit page, works with both post and order object.
 */

namespace Automattic\WooCommerce\Internal\Admin\Orders;

use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;

/**
 * Class Edit.
 */
class Edit {

	/**
	 * Screen ID for the edit order screen.
	 *
	 * @var string
	 */
	private $screen_id;

	/**
	 * Instance of the CustomMetaBox class. Used to render meta box for custom meta.
	 *
	 * @var CustomMetaBox
	 */
	private $custom_meta_box;

	/**
	 * Instance of WC_Order to be used in metaboxes.
	 *
	 * @var \WC_Order
	 */
	private $order;

	/**
	 * Hooks all meta-boxes for order edit page. This is static since this may be called by post edit form rendering.
	 *
	 * @param string $screen_id Screen ID.
	 * @param string $title Title of the page.
	 */
	public static function add_order_meta_boxes( string $screen_id, string $title ) {
		/* Translators: %s order type name. */
		add_meta_box( 'woocommerce-order-data', sprintf( __( '%s data', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Data::output', $screen_id, 'normal', 'high' );
		add_meta_box( 'woocommerce-order-items', __( 'Items', 'woocommerce' ), 'WC_Meta_Box_Order_Items::output', $screen_id, 'normal', 'high' );
		/* Translators: %s order type name. */
		add_meta_box( 'woocommerce-order-notes', sprintf( __( '%s notes', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Notes::output', $screen_id, 'side', 'default' );
		add_meta_box( 'woocommerce-order-downloads', __( 'Downloadable product permissions', 'woocommerce' ) . wc_help_tip( __( 'Note: Permissions for order items will automatically be granted when the order status changes to processing/completed.', 'woocommerce' ) ), 'WC_Meta_Box_Order_Downloads::output', $screen_id, 'normal', 'default' );
		/* Translators: %s order type name. */
		add_meta_box( 'woocommerce-order-actions', sprintf( __( '%s actions', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Actions::output', $screen_id, 'side', 'high' );
	}

	/**
	 * Hooks metabox save functions for order edit page.
	 *
	 * @return void
	 */
	public static function add_save_meta_boxes() {
		/**
		 * Save Order Meta Boxes.
		 *
		 * In order:
		 *      Save the order items.
		 *      Save the order totals.
		 *      Save the order downloads.
		 *      Save order data - also updates status and sends out admin emails if needed. Last to show latest data.
		 *      Save actions - sends out other emails. Last to show latest data.
		 */
		add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Items::save', 10 );
		add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Downloads::save', 30, 2 );
		add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Data::save', 40 );
		add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Actions::save', 50, 2 );
	}

	/**
	 * Enqueue necessary scripts for order edit page.
	 */
	private function enqueue_scripts() {
		if ( wp_is_mobile() ) {
			wp_enqueue_script( 'jquery-touch-punch' );
		}
		wp_enqueue_script( 'post' ); // Ensure existing JS libraries are still available for backward compat.
	}

	/**
	 * Setup hooks, actions and variables needed to render order edit page.
	 *
	 * @param \WC_Order $order Order object.
	 */
	public function setup( \WC_Order $order ) {
		$this->order    = $order;
		$current_screen = get_current_screen();
		$current_screen->is_block_editor( false );
		$this->screen_id = $current_screen->id;
		if ( ! isset( $this->custom_meta_box ) ) {
			$this->custom_meta_box = wc_get_container()->get( CustomMetaBox::class );
		}
		$this->add_save_meta_boxes();
		$this->handle_order_update();
		$this->add_order_meta_boxes( $this->screen_id, __( 'Order', 'woocommerce' ) );
		$this->add_order_specific_meta_box();

		/**
		 * From wp-admin/includes/meta-boxes.php.
		 *
		 * Fires after all built-in meta boxes have been added. Custom metaboxes may be enqueued here.
		 *
		 * @since 3.8.0.
		 */
		do_action( 'add_meta_boxes', wc_get_page_screen_id( 'shop-order' ), $this->order );
		$this->enqueue_scripts();
	}

	/**
	 * Hooks meta box for order specific meta.
	 */
	private function add_order_specific_meta_box() {
		add_meta_box(
			'order_custom',
			__( 'Custom Fields', 'woocommerce' ),
			array( $this, 'render_custom_meta_box' ),
			$this->screen_id,
			'normal'
		);
	}

	/**
	 * Takes care of updating order data. Fires action that metaboxes can hook to for order data updating.
	 *
	 * @return void
	 */
	public function handle_order_update() {
		global $theorder;
		if ( ! isset( $this->order ) ) {
			return;
		}

		if ( 'edit_order' !== sanitize_text_field( wp_unslash( $_POST['action'] ?? '' ) ) ) {
			return;
		}

		check_admin_referer( $this->get_order_edit_nonce_action() );

		/**
		 * Save meta for shop order.
		 *
		 * @param int Order ID.
		 * @param \WC_Order Post object.
		 *
		 * @since 2.1.0
		 */
		do_action( 'woocommerce_process_shop_order_meta', $this->order->get_id(), $this->order );

		// Refresh the order from DB.
		$this->order = wc_get_order( $this->order->get_id() );
		$theorder    = $this->order;
	}

	/**
	 * Helper method to get the name of order edit nonce.
	 *
	 * @return string Nonce action name.
	 */
	private function get_order_edit_nonce_action() {
		return 'update-order_' . $this->order->get_id();
	}

	/**
	 * Render meta box for order specific meta.
	 */
	public function render_custom_meta_box() {
		$this->custom_meta_box->output( $this->order );
	}

	/**
	 * Render order edit page.
	 */
	public function display() {
		$this->render_wrapper_start();
		$this->render_meta_boxes();
		$this->render_wrapper_end();
	}

	/**
	 * Helper function to render wrapper start.
	 *
	 * @param string $notice Notice to display, if any.
	 * @param string $message Message to display, if any.
	 */
	private function render_wrapper_start( $notice = '', $message = '' ) {
		$edit_page_url = admin_url( 'admin.php?page=wc-orders&action=edit&id=' . $this->order->get_id() );
		$form_action   = 'edit_order';
		$referer       = wp_get_referer();
		$new_page_url  = wc_get_container()->get( PageController::class )->get_new_page_url();

		?>
		<div class="wrap">
		<h1 class="wp-heading-inline">
			<?php echo esc_html( 'Edit order' ); ?>
		</h1>
		<?php
		echo ' <a href="' . esc_url( $new_page_url ) . '" class="page-title-action"> Add order </a>';
		?>
		<hr class="wp-header-end">

		<?php
		if ( $notice ) :
			?>
			<div id="notice" class="notice notice-warning"><p
					id="has-newer-autosave"><?php echo wp_kses_post( $notice ); ?></p></div>
		<?php endif; ?>
		<?php if ( $message ) : ?>
			<div id="message" class="updated notice notice-success is-dismissible">
				<p><?php echo wp_kses_post( $message ); ?></p></div>
			<?php
			endif;
		?>

		<form name="order" action="<?php echo esc_url( $edit_page_url ); ?>" method="post" id="order"
		<?php
		/**
		 * Fires inside the order edit form tag.
		 *
		 * @param \WC_Order $order Order object.
		 *
		 * @since 6.9.0
		 */
		do_action( 'order_edit_form_tag', $this->order );
		?>
		>
		<?php wp_nonce_field( $this->get_order_edit_nonce_action() ); ?>
		<input type="hidden" id="hiddenaction" name="action" value="<?php echo esc_attr( $form_action ); ?>"/>
		<input type="hidden" id="original_order_status" name="original_order_status" value="<?php echo esc_attr( $this->order->get_status() ); ?>"/>
		<input type="hidden" id="referredby" name="referredby" value="<?php echo $referer ? esc_url( $referer ) : ''; ?>"/>
		<div id="poststuff">
		<div id="post-body"
		class="metabox-holder columns-<?php echo ( 1 === get_current_screen()->get_columns() ) ? '1' : '2'; ?>">
		<?php
	}

	/**
	 * Helper function to render meta boxes.
	 */
	private function render_meta_boxes() {
		?>
		<div id="postbox-container-1" class="postbox-container">
			<?php do_meta_boxes( $this->screen_id, 'side', $this->order ); ?>
		</div>
		<div id="postbox-container-2" class="postbox-container">
			<?php
			do_meta_boxes( $this->screen_id, 'normal', $this->order );
			do_meta_boxes( $this->screen_id, 'advanced', $this->order );
			?>
		</div>
		<?php
	}

	/**
	 * Helper function to render wrapper end.
	 */
	private function render_wrapper_end() {
		?>
		</div> <!-- /post-body -->
		</div> <!-- /poststuff  -->
		</form>
		</div> <!-- /wrap -->
		<?php
	}
}
Internal/Admin/CouponsMovedTrait.php000064400000004233152333011040013500 0ustar00<?php
/**
 * A Trait to help with managing the legacy coupon menu.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\Features\Features;

/**
 * CouponsMovedTrait trait.
 */
trait CouponsMovedTrait {

	/**
	 * The GET query key for the legacy menu.
	 *
	 * @var string
	 */
	protected static $query_key = 'legacy_coupon_menu';

	/**
	 * The key for storing an option in the DB.
	 *
	 * @var string
	 */
	protected static $option_key = 'wc_admin_show_legacy_coupon_menu';

	/**
	 * Get the URL for the legacy coupon management.
	 *
	 * @return string The unescaped URL for the legacy coupon management page.
	 */
	protected static function get_legacy_coupon_url() {
		return self::get_coupon_url( [ self::$query_key => true ] );
	}

	/**
	 * Get the URL for the coupon management page.
	 *
	 * @param array $args Additional URL query arguments.
	 *
	 * @return string
	 */
	protected static function get_coupon_url( $args = [] ) {
		$args = array_merge(
			[
				'post_type' => 'shop_coupon',
			],
			$args
		);

		return add_query_arg( $args, admin_url( 'edit.php' ) );
	}

	/**
	 * Get the new URL for managing coupons.
	 *
	 * @param string $page The management page.
	 *
	 * @return string
	 */
	protected static function get_management_url( $page ) {
		$path = '';
		switch ( $page ) {
			case 'coupon':
			case 'coupons':
				return self::get_coupon_url();

			case 'marketing':
				$path = self::get_marketing_path();
				break;
		}

		return "wc-admin&path={$path}";
	}

	/**
	 * Get the WC Admin path for the marking page.
	 *
	 * @return string
	 */
	protected static function get_marketing_path() {
		return '/marketing/overview';
	}

	/**
	 * Whether we should display the legacy coupon menu item.
	 *
	 * @return bool
	 */
	protected static function should_display_legacy_menu() {
		return ( get_option( self::$option_key, 1 ) && ! Features::is_enabled( 'navigation' ) );
	}

	/**
	 * Set whether we should display the legacy coupon menu item.
	 *
	 * @param bool $display Whether the menu should be displayed or not.
	 */
	protected static function display_legacy_menu( $display = false ) {
		update_option( self::$option_key, $display ? 1 : 0 );
	}
}
Internal/Admin/Marketing.php000064400000017044152333011040012000 0ustar00<?php
/**
 * WooCommerce Marketing.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Marketing\InstalledExtensions;
use Automattic\WooCommerce\Internal\Admin\Loader;
use Automattic\WooCommerce\Admin\PageController;

/**
 * Contains backend logic for the Marketing feature.
 */
class Marketing {

	use CouponsMovedTrait;

	/**
	 * Name of recommended plugins transient.
	 *
	 * @var string
	 */
	const RECOMMENDED_PLUGINS_TRANSIENT = 'wc_marketing_recommended_plugins';

	/**
	 * Name of knowledge base post transient.
	 *
	 * @var string
	 */
	const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base';

	/**
	 * Class instance.
	 *
	 * @var Marketing instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}

		add_action( 'admin_menu', array( $this, 'register_pages' ), 5 );
		add_action( 'admin_menu', array( $this, 'add_parent_menu_item' ), 6 );

		add_filter( 'woocommerce_admin_preload_options', array( $this, 'preload_options' ) );
		add_filter( 'woocommerce_admin_shared_settings', array( $this, 'component_settings' ), 30 );
	}

	/**
	 * Add main marketing menu item.
	 *
	 * Uses priority of 9 so other items can easily be added at the default priority (10).
	 */
	public function add_parent_menu_item() {
		if ( ! Features::is_enabled( 'navigation' ) ) {
			add_menu_page(
				__( 'Marketing', 'woocommerce' ),
				__( 'Marketing', 'woocommerce' ),
				'manage_woocommerce',
				'woocommerce-marketing',
				null,
				'dashicons-megaphone',
				58
			);
		}

		PageController::get_instance()->connect_page(
			[
				'id'         => 'woocommerce-marketing',
				'title'      => 'Marketing',
				'capability' => 'manage_woocommerce',
				'path'       => 'wc-admin&path=/marketing',
			]
		);
	}

	/**
	 * Registers report pages.
	 */
	public function register_pages() {
		$this->register_overview_page();

		$controller = PageController::get_instance();
		$defaults   = [
			'parent'        => 'woocommerce-marketing',
			'existing_page' => false,
		];

		$marketing_pages = apply_filters( 'woocommerce_marketing_menu_items', [] );
		foreach ( $marketing_pages as $marketing_page ) {
			if ( ! is_array( $marketing_page ) ) {
				continue;
			}

			$marketing_page = array_merge( $defaults, $marketing_page );

			if ( $marketing_page['existing_page'] ) {
				$controller->connect_page( $marketing_page );
			} else {
				$controller->register_page( $marketing_page );
			}
		}
	}

	/**
	 * Register the main Marketing page, which is Marketing > Overview.
	 *
	 * This is done separately because we need to ensure the page is registered properly and
	 * that the link is done properly. For some reason the normal page registration process
	 * gives us the wrong menu link.
	 */
	protected function register_overview_page() {
		global $submenu;

		// First register the page.
		PageController::get_instance()->register_page(
			[
				'id'       => 'woocommerce-marketing-overview',
				'title'    => __( 'Overview', 'woocommerce' ),
				'path'     => 'wc-admin&path=/marketing',
				'parent'   => 'woocommerce-marketing',
				'nav_args' => array(
					'parent' => 'woocommerce-marketing',
					'order'  => 10,
				),
			]
		);

		// Now fix the path, since register_page() gets it wrong.
		if ( ! isset( $submenu['woocommerce-marketing'] ) ) {
			return;
		}

		foreach ( $submenu['woocommerce-marketing'] as &$item ) {
			// The "slug" (aka the path) is the third item in the array.
			if ( 0 === strpos( $item[2], 'wc-admin' ) ) {
				$item[2] = 'admin.php?page=' . $item[2];
			}
		}
	}

	/**
	 * Preload options to prime state of the application.
	 *
	 * @param array $options Array of options to preload.
	 * @return array
	 */
	public function preload_options( $options ) {
		$options[] = 'woocommerce_marketing_overview_welcome_hidden';

		return $options;
	}

	/**
	 * Add settings for marketing feature.
	 *
	 * @param array $settings Component settings.
	 * @return array
	 */
	public function component_settings( $settings ) {
		// Bail early if not on a wc-admin powered page.
		if ( ! PageController::is_admin_page() ) {
			return $settings;
		}

		$settings['marketing']['installedExtensions'] = InstalledExtensions::get_data();

		return $settings;
	}

	/**
	 * Load recommended plugins from WooCommerce.com
	 *
	 * @return array
	 */
	public function get_recommended_plugins() {
		$plugins = get_transient( self::RECOMMENDED_PLUGINS_TRANSIENT );

		if ( false === $plugins ) {
			$request = wp_remote_get(
				'https://woocommerce.com/wp-json/wccom/marketing-tab/1.2/recommendations.json',
				array(
					'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
				)
			);
			$plugins = [];

			if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) {
				$plugins = json_decode( $request['body'], true );
			}

			set_transient(
				self::RECOMMENDED_PLUGINS_TRANSIENT,
				$plugins,
				// Expire transient in 15 minutes if remote get failed.
				// Cache an empty result to avoid repeated failed requests.
				empty( $plugins ) ? 900 : 3 * DAY_IN_SECONDS
			);
		}

		return array_values( $plugins );
	}

	/**
	 * Load knowledge base posts from WooCommerce.com
	 *
	 * @param string $category Category of posts to retrieve.
	 * @return array
	 */
	public function get_knowledge_base_posts( $category ) {

		$kb_transient = self::KNOWLEDGE_BASE_TRANSIENT;

		$categories = array(
			'marketing' => 1744,
			'coupons'   => 25202,
		);

		// Default to marketing category (if no category set on the kb component).
		if ( ! empty( $category ) && array_key_exists( $category, $categories ) ) {
			$category_id  = $categories[ $category ];
			$kb_transient = $kb_transient . '_' . strtolower( $category );
		} else {
			$category_id = $categories['marketing'];
		}

		$posts = get_transient( $kb_transient );

		if ( false === $posts ) {
			$request_url = add_query_arg(
				array(
					'categories' => $category_id,
					'page'       => 1,
					'per_page'   => 8,
					'_embed'     => 1,
				),
				'https://woocommerce.com/wp-json/wp/v2/posts?utm_medium=product'
			);

			$request = wp_remote_get(
				$request_url,
				array(
					'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
				)
			);
			$posts   = [];

			if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) {
				$raw_posts = json_decode( $request['body'], true );

				foreach ( $raw_posts as $raw_post ) {
					$post = [
						'title'         => html_entity_decode( $raw_post['title']['rendered'] ),
						'date'          => $raw_post['date_gmt'],
						'link'          => $raw_post['link'],
						'author_name'   => isset( $raw_post['author_name'] ) ? html_entity_decode( $raw_post['author_name'] ) : '',
						'author_avatar' => isset( $raw_post['author_avatar_url'] ) ? $raw_post['author_avatar_url'] : '',
					];

					$featured_media = $raw_post['_embedded']['wp:featuredmedia'] ?? [];
					if ( count( $featured_media ) > 0 ) {
						$image         = current( $featured_media );
						$post['image'] = add_query_arg(
							array(
								'resize' => '650,340',
								'crop'   => 1,
							),
							$image['source_url']
						);
					}

					$posts[] = $post;
				}
			}

			set_transient(
				$kb_transient,
				$posts,
				// Expire transient in 15 minutes if remote get failed.
				empty( $posts ) ? 900 : DAY_IN_SECONDS
			);
		}

		return $posts;
	}
}
Internal/Admin/Homescreen.php000064400000020361152333011040012143 0ustar00<?php
/**
 * WooCommerce Homescreen.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Shipping;

/**
 * Contains backend logic for the homescreen feature.
 */
class Homescreen {
	/**
	 * Menu slug.
	 */
	const MENU_SLUG = 'wc-admin';

	/**
	 * Class instance.
	 *
	 * @var Homescreen instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
		add_action( 'admin_menu', array( $this, 'register_page' ) );
		// In WC Core 5.1 $submenu manipulation occurs in admin_menu, not admin_head. See https://github.com/woocommerce/woocommerce/pull/29088.
		if ( version_compare( WC_VERSION, '5.1', '>=' ) ) {
			// priority is 20 to run after admin_menu hook for woocommerce runs, so that submenu is populated.
			add_action( 'admin_menu', array( $this, 'possibly_remove_woocommerce_menu' ) );
			add_action( 'admin_menu', array( $this, 'update_link_structure' ), 20 );
		} else {
			// priority is 20 to run after https://github.com/woocommerce/woocommerce/blob/a55ae325306fc2179149ba9b97e66f32f84fdd9c/includes/admin/class-wc-admin-menus.php#L165.
			add_action( 'admin_head', array( $this, 'update_link_structure' ), 20 );
		}

		add_filter( 'woocommerce_admin_preload_options', array( $this, 'preload_options' ) );

		if ( Features::is_enabled( 'shipping-smart-defaults' ) ) {
			add_filter(
				'woocommerce_admin_shared_settings',
				array( $this, 'maybe_set_default_shipping_options_on_home' ),
				9999
			);
		}
	}

	/**
	 * Set free shipping in the same country as the store default
	 * Flag rate in all other countries when any of the following conditions are ture
	 *
	 * - The store sells physical products, has JP and WCS installed and connected, and is located in the US.
	 * - The store sells physical products, and is not located in US/Canada/Australia/UK (irrelevant if JP is installed or not).
	 * - The store sells physical products and is located in US, but JP and WCS are not installed.
	 *
	 * @param array $settings shared admin settings.
	 * @return array
	 */
	public function maybe_set_default_shipping_options_on_home( $settings ) {
		if ( ! function_exists( 'get_current_screen' ) ) {
			return $settings;
		}

		$current_screen = get_current_screen();

		// Abort if it's not the homescreen.
		if ( ! isset( $current_screen->id ) || 'woocommerce_page_wc-admin' !== $current_screen->id ) {
			return $settings;
		}

		// Abort if we already created the shipping options.
		$already_created = get_option( 'woocommerce_admin_created_default_shipping_zones' );
		if ( $already_created === 'yes' ) {
			return $settings;
		}

		$zone_count = count( \WC_Data_Store::load( 'shipping-zone' )->get_zones() );
		if ( $zone_count ) {
			update_option( 'woocommerce_admin_created_default_shipping_zones', 'yes' );
			update_option( 'woocommerce_admin_reviewed_default_shipping_zones', 'yes' );
			return $settings;
		}

		$user_skipped_obw           = $settings['onboarding']['profile']['skipped'] ?? false;
		$store_address              = $settings['preloadSettings']['general']['woocommerce_store_address'] ?? '';
		$product_types              = $settings['onboarding']['profile']['product_types'] ?? array();
		$user_has_set_store_country = $settings['onboarding']['profile']['is_store_country_set'] ?? false;

		// Do not proceed if user has not filled out their country in the onboarding profiler.
		if ( ! $user_has_set_store_country ) {
			return $settings;
		}

		// If user skipped the obw or has not completed the store_details
		// then we assume the user is going to sell physical products.
		if ( $user_skipped_obw || '' === $store_address ) {
			$product_types[] = 'physical';
		}

		if ( false === in_array( 'physical', $product_types, true ) ) {
			return $settings;
		}

		$country_code = wc_format_country_state_string( $settings['preloadSettings']['general']['woocommerce_default_country'] )['country'];
		$country_name = WC()->countries->get_countries()[ $country_code ] ?? null;

		$is_jetpack_installed = in_array( 'jetpack', $settings['plugins']['installedPlugins'] ?? array(), true );
		$is_wcs_installed     = in_array( 'woocommerce-services', $settings['plugins']['installedPlugins'] ?? array(), true );

		if (
			( 'US' === $country_code && $is_jetpack_installed )
			||
			( ! in_array( $country_code, array( 'US', 'CA', 'AU', 'GB' ), true ) )
			||
			( 'US' === $country_code && false === $is_jetpack_installed && false === $is_wcs_installed )
		) {
			$zone = new \WC_Shipping_Zone();
			$zone->set_zone_name( $country_name );
			$zone->add_location( $country_code, 'country' );
			$zone->add_shipping_method( 'free_shipping' );
			update_option( 'woocommerce_admin_created_default_shipping_zones', 'yes' );
			Shipping::delete_zone_count_transient();
		}

		return $settings;
	}

	/**
	 * Adds fields so that we can store performance indicators, row settings, and chart type settings for users.
	 *
	 * @param array $user_data_fields User data fields.
	 * @return array
	 */
	public function add_user_data_fields( $user_data_fields ) {
		return array_merge(
			$user_data_fields,
			array(
				'homepage_layout',
				'homepage_stats',
				'task_list_tracked_started_tasks',
				'help_panel_highlight_shown',
			)
		);
	}

	/**
	 * Registers home page.
	 */
	public function register_page() {
		// Register a top-level item for users who cannot view the core WooCommerce menu.
		if ( ! self::is_admin_user() ) {
			wc_admin_register_page(
				array(
					'id'         => 'woocommerce-home',
					'title'      => __( 'WooCommerce', 'woocommerce' ),
					'path'       => self::MENU_SLUG,
					'capability' => 'read',
				)
			);
			return;
		}

		wc_admin_register_page(
			array(
				'id'         => 'woocommerce-home',
				'title'      => __( 'Home', 'woocommerce' ),
				'parent'     => 'woocommerce',
				'path'       => self::MENU_SLUG,
				'order'      => 0,
				'capability' => 'read',
			)
		);
	}

	/**
	 * Check if the user can access the top-level WooCommerce item.
	 *
	 * @return bool
	 */
	public static function is_admin_user() {
		if ( ! class_exists( 'WC_Admin_Menus', false ) ) {
			include_once WC_ABSPATH . 'includes/admin/class-wc-admin-menus.php';
		}
		if ( method_exists( 'WC_Admin_Menus', 'can_view_woocommerce_menu_item' ) ) {
			return \WC_Admin_Menus::can_view_woocommerce_menu_item() || current_user_can( 'manage_woocommerce' );
		} else {
			// We leave this line for WC versions <= 6.2.
			return current_user_can( 'edit_others_shop_orders' ) || current_user_can( 'manage_woocommerce' );
		}
	}

	/**
	 * Possibly remove the WooCommerce menu item if it was purely used to access wc-admin pages.
	 */
	public function possibly_remove_woocommerce_menu() {
		global $menu;

		if ( self::is_admin_user() ) {
			return;
		}

		foreach ( $menu as $key => $menu_item ) {
			if ( self::MENU_SLUG !== $menu_item[2] || 'read' !== $menu_item[1] ) {
				continue;
			}

			unset( $menu[ $key ] );
		}
	}

	/**
	 * Update the WooCommerce menu structure to make our main dashboard/handler
	 * the top level link for 'WooCommerce'.
	 */
	public function update_link_structure() {
		global $submenu;
		// User does not have capabilites to see the submenu.
		if ( ! current_user_can( 'manage_woocommerce' ) || empty( $submenu['woocommerce'] ) ) {
			return;
		}

		$wc_admin_key = null;
		foreach ( $submenu['woocommerce'] as $submenu_key => $submenu_item ) {
			if ( self::MENU_SLUG === $submenu_item[2] ) {
				$wc_admin_key = $submenu_key;
				break;
			}
		}

		if ( ! $wc_admin_key ) {
			return;
		}

		$menu = $submenu['woocommerce'][ $wc_admin_key ];

		// Move menu item to top of array.
		unset( $submenu['woocommerce'][ $wc_admin_key ] );
		array_unshift( $submenu['woocommerce'], $menu );
	}

	/**
	 * Preload options to prime state of the application.
	 *
	 * @param array $options Array of options to preload.
	 * @return array
	 */
	public function preload_options( $options ) {
		$options[] = 'woocommerce_default_homepage_layout';
		$options[] = 'woocommerce_admin_install_timestamp';

		return $options;
	}
}
Internal/Admin/SettingsNavigationFeature.php000064400000010521152333011040015204 0ustar00<?php
/**
 * WooCommerce Settings.
 */

namespace Automattic\WooCommerce\Internal\Admin;

use Automattic\WooCommerce\Admin\PageController;

/**
 * Contains backend logic for the Settings feature.
 */
class SettingsNavigationFeature {
	/**
	 * Option name used to toggle this feature.
	 */
	const TOGGLE_OPTION_NAME = 'woocommerce_settings_enabled';

	/**
	 * Class instance.
	 *
	 * @var Settings instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		if ( ! is_admin() ) {
			return;
		}

		add_filter( 'woocommerce_settings_features', array( $this, 'add_feature_toggle' ) );

		if ( 'yes' !== get_option( 'woocommerce_settings_enabled', 'no' ) ) {
			return;
		}

		add_filter( 'woocommerce_admin_shared_settings', array( __CLASS__, 'add_component_settings' ) );
		// Run this after the original WooCommerce settings have been added.
		add_action( 'admin_menu', array( $this, 'register_pages' ), 60 );
		add_action( 'init', array( $this, 'redirect_core_settings_pages' ) );
	}

	/**
	 * Add the necessary data to initially load the WooCommerce Settings pages.
	 *
	 * @param array $settings Array of component settings.
	 * @return array Array of component settings.
	 */
	public static function add_component_settings( $settings ) {
		if ( ! is_admin() ) {
			return $settings;
		}

		$setting_pages = \WC_Admin_Settings::get_settings_pages();
		$pages         = array();
		foreach ( $setting_pages as $setting_page ) {
			$pages = $setting_page->add_settings_page( $pages );
		}

		$settings['settingsPages'] = $pages;

		return $settings;
	}

	/**
	 * Add the feature toggle to the features settings.
	 *
	 * @param array $features Feature sections.
	 * @return array
	 */
	public static function add_feature_toggle( $features ) {
		$features[] = array(
			'title' => __( 'Settings', 'woocommerce' ),
			'desc'  => __(
				'Adds the new WooCommerce settings UI.',
				'woocommerce'
			),
			'id'    => 'woocommerce_settings_enabled',
			'type'  => 'checkbox',
		);

		return $features;
	}

	/**
	 * Registers settings pages.
	 */
	public function register_pages() {
		$controller = PageController::get_instance();

		$setting_pages = \WC_Admin_Settings::get_settings_pages();
		$settings      = array();
		foreach ( $setting_pages as $setting_page ) {
			$settings = $setting_page->add_settings_page( $settings );
		}

		$order = 0;
		foreach ( $settings as $key => $setting ) {
			$order        += 10;
			$settings_page = array(
				'parent'   => 'woocommerce-settings',
				'title'    => $setting,
				'id'       => 'settings-' . $key,
				'path'     => "/settings/$key",
				'nav_args' => array(
					'capability' => 'manage_woocommerce',
					'order'      => $order,
					'parent'     => 'woocommerce-settings',
				),
			);

			// Replace the old menu with the first settings item.
			if ( 10 === $order ) {
				$this->replace_settings_page( $settings_page );
			}

			$controller->register_page( $settings_page );
		}
	}

	/**
	 * Replace the Settings page in the original WooCommerce menu.
	 *
	 * @param array $page Page used to replace the original.
	 */
	protected function replace_settings_page( $page ) {
		global $submenu;

		// Check if WooCommerce parent menu has been registered.
		if ( ! isset( $submenu['woocommerce'] ) ) {
			return;
		}

		foreach ( $submenu['woocommerce'] as &$item ) {
			// The "slug" (aka the path) is the third item in the array.
			if ( 0 === strpos( $item[2], 'wc-settings' ) ) {
				$item[2] = wc_admin_url( "&path={$page['path']}" );
			}
		}
	}

	/**
	 * Redirect the old settings page URLs to the new ones.
	 */
	public function redirect_core_settings_pages() {
		/* phpcs:disable WordPress.Security.NonceVerification */
		if ( ! isset( $_GET['page'] ) || 'wc-settings' !== $_GET['page'] ) {
			return;
		}

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			return;
		}

		$setting_pages   = \WC_Admin_Settings::get_settings_pages();
		$default_setting = isset( $setting_pages[0] ) ? $setting_pages[0]->get_id() : '';
		$setting         = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : $default_setting;
		/* phpcs:enable */

		wp_safe_redirect( wc_admin_url( "&path=/settings/$setting" ) );
		exit;
	}
}
Internal/Admin/WCPayPromotion/Init.php000064400000011643152333011040013653 0ustar00<?php
/**
 * Handles wcpay promotion
 */

namespace Automattic\WooCommerce\Internal\Admin\WCPayPromotion;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\DataSourcePoller;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\EvaluateSuggestion;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\PaymentGatewaySuggestionsDataSourcePoller as PaymentGatewaySuggestionsDataSourcePoller;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * WC Pay Promotion engine.
 */
class Init {
	const EXPLAT_VARIATION_PREFIX = 'woocommerce_wc_pay_promotion_payment_methods_table_';

	/**
	 * Constructor.
	 */
	public function __construct() {
		include_once __DIR__ . '/WCPaymentGatewayPreInstallWCPayPromotion.php';

		$is_payments_page = isset( $_GET['page'] ) && $_GET['page'] === 'wc-settings' && isset( $_GET['tab'] ) && $_GET['tab'] === 'checkout'; // phpcs:ignore WordPress.Security.NonceVerification
		if ( ! wp_is_json_request() && ! $is_payments_page ) {
			return;
		}

		add_filter( 'woocommerce_payment_gateways', array( __CLASS__, 'possibly_register_pre_install_wc_pay_promotion_gateway' ) );
		add_filter( 'option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ] );
		add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ] );

		$rtl = is_rtl() ? '.rtl' : '';

		wp_enqueue_style(
			'wc-admin-payment-method-promotions',
			WCAdminAssets::get_url( "payment-method-promotions/style{$rtl}", 'css' ),
			array( 'wp-components' ),
			WCAdminAssets::get_file_version( 'css' )
		);

		WCAdminAssets::register_script( 'wp-admin-scripts', 'payment-method-promotions', true );
	}

	/**
	 * Possibly registers the pre install wc pay promoted gateway.
	 *
	 * @param array $gateways list of gateway classes.
	 * @return array list of gateway classes.
	 */
	public static function possibly_register_pre_install_wc_pay_promotion_gateway( $gateways ) {
		if ( self::can_show_promotion() && ! WCPaymentGatewayPreInstallWCPayPromotion::is_dismissed() ) {
			$gateways[] = 'Automattic\WooCommerce\Internal\Admin\WCPayPromotion\WCPaymentGatewayPreInstallWCPayPromotion';
		}
		return $gateways;
	}

	/**
	 * Checks if promoted gateway can be registered.
	 *
	 * @return boolean if promoted gateway should be registered.
	 */
	public static function can_show_promotion() {
		// Check if WC Pay is enabled.
		if ( class_exists( '\WC_Payments' ) ) {
			return false;
		}
		if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) === 'no' ) {
			return false;
		}
		if ( ! apply_filters( 'woocommerce_allow_marketplace_suggestions', true ) ) {
			return false;
		}

		$wc_pay_spec = self::get_wc_pay_promotion_spec();
		if ( ! $wc_pay_spec ) {
			return false;
		}
		return true;
	}

	/**
	 * By default, new payment gateways are put at the bottom of the list on the admin "Payments" settings screen.
	 * For visibility, we want WooCommerce Payments to be at the top of the list.
	 *
	 * @param array $ordering Existing ordering of the payment gateways.
	 *
	 * @return array Modified ordering.
	 */
	public static function set_gateway_top_of_list( $ordering ) {
		$ordering = (array) $ordering;
		$id       = WCPaymentGatewayPreInstallWCPayPromotion::GATEWAY_ID;
		// Only tweak the ordering if the list hasn't been reordered with WooCommerce Payments in it already.
		if ( ! isset( $ordering[ $id ] ) || ! is_numeric( $ordering[ $id ] ) ) {
			$is_empty        = empty( $ordering ) || ( count( $ordering ) === 1 && $ordering[0] === false );
			$ordering[ $id ] = $is_empty ? 0 : ( min( $ordering ) - 1 );
		}
		return $ordering;
	}

	/**
	 * Get WC Pay promotion spec.
	 */
	public static function get_wc_pay_promotion_spec() {
		$promotions            = self::get_promotions();
		$wc_pay_promotion_spec = array_values(
			array_filter(
				$promotions,
				function( $promotion ) {
					return isset( $promotion->plugins ) && in_array( 'woocommerce-payments', $promotion->plugins, true );
				}
			)
		);

		return current( $wc_pay_promotion_spec );
	}

	/**
	 * Go through the specs and run them.
	 */
	public static function get_promotions() {
		$suggestions = array();
		$specs       = self::get_specs();

		foreach ( $specs as $spec ) {
			$suggestion    = EvaluateSuggestion::evaluate( $spec );
			$suggestions[] = $suggestion;
		}

		return array_values(
			array_filter(
				$suggestions,
				function( $suggestion ) {
					return ! property_exists( $suggestion, 'is_visible' ) || $suggestion->is_visible;
				}
			)
		);

	}

	/**
	 * Delete the specs transient.
	 */
	public static function delete_specs_transient() {
		WCPayPromotionDataSourcePoller::get_instance()->delete_specs_transient();
	}

	/**
	 * Get specs or fetch remotely if they don't exist.
	 */
	public static function get_specs() {
		if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) === 'no' ) {
			return array();
		}
		return WCPayPromotionDataSourcePoller::get_instance()->get_specs_from_data_sources();
	}
}

Internal/Admin/WCPayPromotion/WCPaymentGatewayPreInstallWCPayPromotion.php000064400000003224152333011040022746 0ustar00<?php
/**
 * Class WCPaymentGatewayPreInstallWCPayPromotion
 *
 * @package WooCommerce\Admin
 */

namespace Automattic\WooCommerce\Internal\Admin\WCPayPromotion;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * A Psuedo WCPay gateway class.
 *
 * @extends WC_Payment_Gateway
 */
class WCPaymentGatewayPreInstallWCPayPromotion extends \WC_Payment_Gateway {

	const GATEWAY_ID = 'pre_install_woocommerce_payments_promotion';

	/**
	 * Constructor
	 */
	public function __construct() {
		$wc_pay_spec = Init::get_wc_pay_promotion_spec();
		if ( ! $wc_pay_spec ) {
			return;
		}
		$this->id           = static::GATEWAY_ID;
		$this->method_title = $wc_pay_spec->title;
		if ( property_exists( $wc_pay_spec, 'sub_title' ) ) {
			$this->title = sprintf( '<span class="gateway-subtitle" >%s</span>', $wc_pay_spec->sub_title );
		}
		$this->method_description = $wc_pay_spec->content;
		$this->has_fields         = false;

		// Get setting values.
		$this->enabled = false;

		// Load the settings.
		$this->init_form_fields();
		$this->init_settings();
	}

	/**
	 * Initialise Gateway Settings Form Fields.
	 */
	public function init_form_fields() {
		$this->form_fields = array(
			'is_dismissed' => array(
				'title'   => __( 'Dismiss', 'woocommerce' ),
				'type'    => 'checkbox',
				'label'   => __( 'Dismiss the gateway', 'woocommerce' ),
				'default' => 'no',
			),
		);
	}

	/**
	 * Check if the promotional gateaway has been dismissed.
	 *
	 * @return bool
	 */
	public static function is_dismissed() {
		$settings = get_option( 'woocommerce_' . self::GATEWAY_ID . '_settings', array() );
		return isset( $settings['is_dismissed'] ) && 'yes' === $settings['is_dismissed'];
	}
}
Internal/Admin/WCPayPromotion/WCPayPromotionDataSourcePoller.php000064400000001425152333011040020770 0ustar00<?php
namespace Automattic\WooCommerce\Internal\Admin\WCPayPromotion;

use Automattic\WooCommerce\Admin\DataSourcePoller;

/**
 * Specs data source poller class for WooCommerce Payment Promotion.
 */
class WCPayPromotionDataSourcePoller extends DataSourcePoller {

	const ID = 'payment_method_promotion';

	/**
	 * Default data sources array.
	 */
	const DATA_SOURCES = array(
		'https://woocommerce.com/wp-json/wccom/payment-gateway-suggestions/1.0/payment-method/promotions.json',
	);

	/**
	 * Class instance.
	 *
	 * @var Analytics instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self( self::ID, self::DATA_SOURCES );
		}
		return self::$instance;
	}
}
Internal/Admin/Survey.php000064400000001400152333011040011341 0ustar00<?php
/**
 * Survey helper methods.
 */

namespace Automattic\WooCommerce\Internal\Admin;

defined( 'ABSPATH' ) || exit;

/**
 * Survey Class.
 */
class Survey {
	/**
	 * Survey URL.
	 */
	const SURVEY_URL = 'https://automattic.survey.fm';

	/**
	 * Get a survey's URL from a path.
	 *
	 * @param  string $path Path of the survey.
	 * @param  array  $query Query arguments as key value pairs.
	 * @return string Full URL to survey.
	 */
	public static function get_url( $path, $query = array() ) {
		$url = self::SURVEY_URL . $path;

		$query_args = apply_filters( 'woocommerce_admin_survey_query', $query );

		if ( ! empty( $query_args ) ) {
			$query_string = http_build_query( $query_args );
			$url          = $url . '?' . $query_string;
		}

		return $url;
	}
}
Internal/RestockRefundedItemsAdjuster.php000064400000004121152333011040014612 0ustar00<?php
/**
 * RestockRefundedItemsAdjuster class file.
 */

namespace Automattic\WooCommerce\Internal;

use Automattic\WooCommerce\Proxies\LegacyProxy;

defined( 'ABSPATH' ) || exit;

/**
 * Class to adjust or initialize the restock refunded items.
 */
class RestockRefundedItemsAdjuster {
	/**
	 * The order factory to use.
	 *
	 * @var WC_Order_Factory
	 */
	private $order_factory;

	/**
	 * Class initialization, to be executed when the class is resolved by the container.
	 *
	 * @internal
	 */
	final public function init() {
		$this->order_factory = wc_get_container()->get( LegacyProxy::class )->get_instance_of( \WC_Order_Factory::class );
		add_action( 'woocommerce_before_save_order_items', array( $this, 'initialize_restock_refunded_items' ), 10, 2 );
	}

	/**
	 * Initializes the restock refunded items meta for order version less than 5.5.
	 *
	 * @see https://github.com/woocommerce/woocommerce/issues/29502
	 *
	 * @param int   $order_id Order ID.
	 * @param array $items Order items to save.
	 */
	public function initialize_restock_refunded_items( $order_id, $items ) {
		$order         = wc_get_order( $order_id );
		$order_version = $order->get_version();

		if ( version_compare( $order_version, '5.5', '>=' ) ) {
			return;
		}

		// If there are no refund lines, then this migration isn't necessary because restock related meta's wouldn't be set.
		if ( 0 === count( $order->get_refunds() ) ) {
			return;
		}

		if ( isset( $items['order_item_id'] ) ) {
			foreach ( $items['order_item_id'] as $item_id ) {
				$item = $this->order_factory::get_order_item( absint( $item_id ) );

				if ( ! $item ) {
					continue;
				}

				if ( 'line_item' !== $item->get_type() ) {
					continue;
				}

				// There could be code paths in custom code which don't update version number but still update the items.
				if ( '' !== $item->get_meta( '_restock_refunded_items', true ) ) {
					continue;
				}

				$refunded_item_quantity = abs( $order->get_qty_refunded_for_item( $item->get_id() ) );
				$item->add_meta_data( '_restock_refunded_items', $refunded_item_quantity, false );
				$item->save();
			}
		}
	}
}
Internal/DependencyManagement/ExtendedContainer.php000064400000013714152333011040016505 0ustar00<?php
/**
 * ExtendedContainer class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement;

use Automattic\WooCommerce\Container;
use Automattic\WooCommerce\Utilities\StringUtil;
use Automattic\WooCommerce\Vendor\League\Container\Container as BaseContainer;
use Automattic\WooCommerce\Vendor\League\Container\Definition\DefinitionInterface;

/**
 * This class extends the original League's Container object by adding some functionality
 * that we need for WooCommerce.
 */
class ExtendedContainer extends BaseContainer {

	/**
	 * The root namespace of all WooCommerce classes in the `src` directory.
	 *
	 * @var string
	 */
	private $woocommerce_namespace = 'Automattic\\WooCommerce\\';

	/**
	 * Whitelist of classes that we can register using the container
	 * despite not belonging to the WooCommerce root namespace.
	 *
	 * In general we allow only the registration of classes in the
	 * WooCommerce root namespace to prevent registering 3rd party code
	 * (which doesn't really belong to this container) or old classes
	 * (which may be eventually deprecated, also the LegacyProxy
	 * should be used for those).
	 *
	 * @var string[]
	 */
	private $registration_whitelist = array(
		Container::class,
	);

	/**
	 * Register a class in the container.
	 *
	 * @param string    $class_name Class name.
	 * @param mixed     $concrete How to resolve the class with `get`: a factory callback, a concrete instance, another class name, or null to just create an instance of the class.
	 * @param bool|null $shared Whether the resolution should be performed only once and cached.
	 *
	 * @return DefinitionInterface The generated definition for the container.
	 * @throws ContainerException Invalid parameters.
	 */
	public function add( string $class_name, $concrete = null, bool $shared = null ) : DefinitionInterface {
		if ( ! $this->is_class_allowed( $class_name ) ) {
			throw new ContainerException( "You cannot add '$class_name', only classes in the {$this->woocommerce_namespace} namespace are allowed." );
		}

		$concrete_class = $this->get_class_from_concrete( $concrete );
		if ( isset( $concrete_class ) && ! $this->is_class_allowed( $concrete_class ) ) {
			throw new ContainerException( "You cannot add concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." );
		}

		// We want to use a definition class that does not support constructor injection to avoid accidental usage.
		if ( ! $concrete instanceof DefinitionInterface ) {
			$concrete = new Definition( $class_name, $concrete );
		}

		return parent::add( $class_name, $concrete, $shared );
	}

	/**
	 * Replace an existing registration with a different concrete.
	 *
	 * @param string $class_name The class name whose definition will be replaced.
	 * @param mixed  $concrete The new concrete (same as "add").
	 *
	 * @return DefinitionInterface The modified definition.
	 * @throws ContainerException Invalid parameters.
	 */
	public function replace( string $class_name, $concrete ) : DefinitionInterface {
		if ( ! $this->has( $class_name ) ) {
			throw new ContainerException( "The container doesn't have '$class_name' registered, please use 'add' instead of 'replace'." );
		}

		$concrete_class = $this->get_class_from_concrete( $concrete );
		if ( isset( $concrete_class ) && ! $this->is_class_allowed( $concrete_class ) && ! $this->is_anonymous_class( $concrete_class ) ) {
			throw new ContainerException( "You cannot use concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." );
		}

		return $this->extend( $class_name )->setConcrete( $concrete );
	}

	/**
	 * Reset all the cached resolutions, so any further "get" for shared definitions will generate the instance again.
	 */
	public function reset_all_resolved() {
		foreach ( $this->definitions->getIterator() as $definition ) {
			// setConcrete causes the cached resolved value to be forgotten.
			$concrete = $definition->getConcrete();
			$definition->setConcrete( $concrete );
		}
	}

	/**
	 * Get an instance of a registered class.
	 *
	 * @param string $id The class name.
	 * @param bool   $new True to generate a new instance even if the class was registered as shared.
	 *
	 * @return object An instance of the requested class.
	 * @throws ContainerException Attempt to get an instance of a non-namespaced class.
	 */
	public function get( $id, bool $new = false ) {
		if ( false === strpos( $id, '\\' ) ) {
			throw new ContainerException( "Attempt to get an instance of the non-namespaced class '$id' from the container, did you forget to add a namespace import?" );
		}

		return parent::get( $id, $new );
	}

	/**
	 * Gets the class from the concrete regardless of type.
	 *
	 * @param mixed $concrete The concrete that we want the class from..
	 *
	 * @return string|null The class from the concrete if one is available, null otherwise.
	 */
	protected function get_class_from_concrete( $concrete ) {
		if ( is_object( $concrete ) && ! is_callable( $concrete ) ) {
			if ( $concrete instanceof DefinitionInterface ) {
				return $this->get_class_from_concrete( $concrete->getConcrete() );
			}

			return get_class( $concrete );
		}

		if ( is_string( $concrete ) && class_exists( $concrete ) ) {
			return $concrete;
		}

		return null;
	}

	/**
	 * Checks to see whether or not a class is allowed to be registered.
	 *
	 * @param string $class_name The class to check.
	 *
	 * @return bool True if the class is allowed to be registered, false otherwise.
	 */
	protected function is_class_allowed( string $class_name ): bool {
		return StringUtil::starts_with( $class_name, $this->woocommerce_namespace, false ) || in_array( $class_name, $this->registration_whitelist, true );
	}

	/**
	 * Check if a class name corresponds to an anonymous class.
	 *
	 * @param string $class_name The class name to check.
	 * @return bool True if the name corresponds to an anonymous class.
	 */
	protected function is_anonymous_class( string $class_name ): bool {
		return StringUtil::starts_with( $class_name, 'class@anonymous' );
	}
}
Internal/DependencyManagement/ServiceProviders/ProductDownloadsServiceProvider.php000064400000002204152333011040024717 0ustar00<?php

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin\SyncUI;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin\UI;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchronize;

/**
 * Service provider for the Product Downloads-related services.
 */
class ProductDownloadsServiceProvider extends AbstractServiceProvider {
	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		Register::class,
		Sync::class,
		SyncUI::class,
		UI::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( Register::class );
		$this->share( Synchronize::class )->addArgument( Register::class );
		$this->share( SyncUI::class )->addArgument( Register::class );
		$this->share( UI::class )->addArgument( Register::class );
	}
}
Internal/DependencyManagement/ServiceProviders/UtilsClassesServiceProvider.php000064400000002770152333011040024052 0ustar00<?php
/**
 * UtilsClassesServiceProvider class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\Utilities\COTMigrationUtil;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Internal\Utilities\HtmlSanitizer;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\PluginUtil;
use Automattic\WooCommerce\Utilities\OrderUtil;

/**
 * Service provider for the non-static utils classes in the Automattic\WooCommerce\src namespace.
 */
class UtilsClassesServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		DatabaseUtil::class,
		HtmlSanitizer::class,
		OrderUtil::class,
		PluginUtil::class,
		COTMigrationUtil::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( DatabaseUtil::class );
		$this->share( HtmlSanitizer::class );
		$this->share( OrderUtil::class );
		$this->share( PluginUtil::class )
			->addArgument( LegacyProxy::class );
		$this->share( COTMigrationUtil::class )
			->addArguments( array( CustomOrdersTableController::class, DataSynchronizer::class ) );
	}
}
Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php000064400000001464152333011040023064 0ustar00<?php
/**
 * ProxiesServiceProvider class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Proxies\ActionsProxy;

/**
 * Service provider for the classes in the Automattic\WooCommerce\Proxies namespace.
 */
class ProxiesServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		LegacyProxy::class,
		ActionsProxy::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( ActionsProxy::class );
		$this->share_with_auto_arguments( LegacyProxy::class );
	}
}
Internal/DependencyManagement/ServiceProviders/FeaturesServiceProvider.php000064400000001501152333011040023201 0ustar00<?php

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\PluginUtil;

/**
 * Service provider for the features enabling/disabling/compatibility engine.
 */
class FeaturesServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		FeaturesController::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( FeaturesController::class )
			->addArguments( array( LegacyProxy::class, PluginUtil::class ) );
	}
}
Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php000064400000001364152333011040027137 0ustar00<?php
/**
 * DownloadPermissionsAdjusterServiceProvider class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;

/**
 * Service provider for the DownloadPermissionsAdjuster class.
 */
class DownloadPermissionsAdjusterServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		DownloadPermissionsAdjuster::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( DownloadPermissionsAdjuster::class );
	}
}
Internal/DependencyManagement/ServiceProviders/OptionSanitizerServiceProvider.php000064400000001266152333011040024574 0ustar00<?php
/**
 * OptionSanitizerServiceProvider class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\Settings\OptionSanitizer;

/**
 * Service provider for the OptionSanitizer class.
 */
class OptionSanitizerServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		OptionSanitizer::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( OptionSanitizer::class );
	}

}
Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php000064400000005124152333011040024475 0ustar00<?php
/**
 * OrdersDataStoreServiceProvider class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableRefundDataStore;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStoreMeta;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;

/**
 * Service provider for the classes in the Internal\DataStores\Orders namespace.
 */
class OrdersDataStoreServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		DataSynchronizer::class,
		CustomOrdersTableController::class,
		OrdersTableDataStore::class,
		CLIRunner::class,
		OrdersTableDataStoreMeta::class,
		OrdersTableRefundDataStore::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( OrdersTableDataStoreMeta::class );

		$this->share( OrdersTableDataStore::class )->addArguments( array( OrdersTableDataStoreMeta::class, DatabaseUtil::class, LegacyProxy::class ) );
		$this->share( DataSynchronizer::class )->addArguments( array( OrdersTableDataStore::class, DatabaseUtil::class, PostsToOrdersMigrationController::class, LegacyProxy::class ) );
		$this->share( OrdersTableRefundDataStore::class )->addArguments( array( OrdersTableDataStoreMeta::class, DatabaseUtil::class, LegacyProxy::class ) );
		$this->share( CustomOrdersTableController::class )->addArguments(
			array(
				OrdersTableDataStore::class,
				DataSynchronizer::class,
				OrdersTableRefundDataStore::class,
				BatchProcessingController::class,
				FeaturesController::class,
			)
		);

		if ( Constants::is_defined( 'WP_CLI' ) && WP_CLI ) {
			$this->share( CLIRunner::class )->addArguments( array( CustomOrdersTableController::class, DataSynchronizer::class, PostsToOrdersMigrationController::class ) );
		}
	}
}
Internal/DependencyManagement/ServiceProviders/BatchProcessingServiceProvider.php000064400000001750152333011040024507 0ustar00<?php
/**
 * Service provider for ActionUpdateController class.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;

/**
 * Class BatchProcessingServiceProvider
 *
 * @package Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders
 */
class BatchProcessingServiceProvider extends AbstractServiceProvider {

	/**
	 * Services provided by this provider.
	 *
	 * @var string[]
	 */
	protected $provides = array(
		BatchProcessingController::class,
	);

	/**
	 * Use the register method to register items with the container via the
	 * protected $this->leagueContainer property or the `getLeagueContainer` method
	 * from the ContainerAwareTrait.
	 *
	 * @return void
	 */
	public function register() {
		$this->share( BatchProcessingController::class, new BatchProcessingController() );
	}
}
Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php000064400000002224152333011040023725 0ustar00<?php
/**
 * Service provider for COTMigration.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;

/**
 * Class COTMigrationServiceProvider
 *
 * @package Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders
 */
class COTMigrationServiceProvider extends AbstractServiceProvider {

	/**
	 * Services provided by this provider.
	 *
	 * @var string[]
	 */
	protected $provides = array(
		PostsToOrdersMigrationController::class,
		CLIRunner::class,
		DataSynchronizer::class,
	);

	/**
	 * Use the register method to register items with the container via the
	 * protected $this->leagueContainer property or the `getLeagueContainer` method
	 * from the ContainerAwareTrait.
	 *
	 * @return void
	 */
	public function register() {
		$this->share( PostsToOrdersMigrationController::class );
	}
}
Internal/DependencyManagement/ServiceProviders/ProductReviewsServiceProvider.php000064400000001562152333011040024417 0ustar00<?php
/**
 * OrdersDataStoreServiceProvider class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews;
use Automattic\WooCommerce\Internal\Admin\ProductReviews\ReviewsCommentsOverrides;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;

/**
 * Service provider for the classes in the Internal\Admin\ProductReviews namespace.
 */
class ProductReviewsServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		Reviews::class,
		ReviewsCommentsOverrides::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( Reviews::class );
		$this->share( ReviewsCommentsOverrides::class );
	}
}
Internal/DependencyManagement/ServiceProviders/ProductAttributesLookupServiceProvider.php000064400000002112152333011040026303 0ustar00<?php
/**
 * ProductAttributesLookupServiceProvider class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;

/**
 * Service provider for the ProductAttributesLookupServiceProvider namespace.
 */
class ProductAttributesLookupServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		DataRegenerator::class,
		Filterer::class,
		LookupDataStore::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( DataRegenerator::class )->addArgument( LookupDataStore::class );
		$this->share( Filterer::class )->addArgument( LookupDataStore::class );
		$this->share( LookupDataStore::class );
	}
}
Internal/DependencyManagement/ServiceProviders/AssignDefaultCategoryServiceProvider.php000064400000001320152333011040025651 0ustar00<?php
/**
 * AssignDefaultCategoryServiceProvider class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\AssignDefaultCategory;

/**
 * Service provider for the AssignDefaultCategory class.
 */
class AssignDefaultCategoryServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		AssignDefaultCategory::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( AssignDefaultCategory::class );
	}
}
Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php000064400000002177152333011040023461 0ustar00<?php
/**
 * Service provider for various order admin classes.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController;
use Automattic\WooCommerce\Internal\Admin\Orders\Edit;
use Automattic\WooCommerce\Internal\Admin\Orders\ListTable;
use Automattic\WooCommerce\Internal\Admin\Orders\PageController;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;

/**
 * OrderAdminServiceProvider class.
 */
class OrderAdminServiceProvider extends AbstractServiceProvider {

	/**
	 * List services provided by this class.
	 *
	 * @var string[]
	 */
	protected $provides = array(
		COTRedirectionController::class,
		PageController::class,
		Edit::class,
		ListTable::class,
	);

	/**
	 * Registers services provided by this class.
	 *
	 * @return void
	 */
	public function register() {
		$this->share( COTRedirectionController::class );
		$this->share( PageController::class );
		$this->share( Edit::class )->addArgument( PageController::class );
		$this->share( ListTable::class )->addArgument( PageController::class );
	}
}
Internal/DependencyManagement/ServiceProviders/RestockRefundedItemsAdjusterServiceProvider.php000064400000001372152333011040027224 0ustar00<?php
/**
 * RestockRefundedItemsAdjusterServiceProvider class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\RestockRefundedItemsAdjuster;

/**
 * Service provider for the RestockRefundedItemsAdjuster class.
 */
class RestockRefundedItemsAdjusterServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		RestockRefundedItemsAdjuster::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( RestockRefundedItemsAdjuster::class );
	}
}
Internal/DependencyManagement/ServiceProviders/OrderMetaBoxServiceProvider.php000064400000001252152333011040023761 0ustar00<?php
/**
 * Service provider for order meta boxes.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;

/**
 * OrderMetaBoxServiceProvider class.
 */
class OrderMetaBoxServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		CustomMetaBox::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( CustomMetaBox::class );
	}

}
Internal/DependencyManagement/ServiceProviders/OrdersControllersServiceProvider.php000064400000001571152333011040025117 0ustar00<?php
/**
 * OrdersControllersServiceProvider class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\Orders\CouponsController;
use Automattic\WooCommerce\Internal\Orders\TaxesController;

/**
 * Service provider for the orders controller classes in the Automattic\WooCommerce\Internal\Orders namespace.
 */
class OrdersControllersServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		CouponsController::class,
		TaxesController::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( CouponsController::class );
		$this->share( TaxesController::class );
	}
}
Internal/DependencyManagement/ServiceProviders/ObjectCacheServiceProvider.php000064400000001154152333011040023561 0ustar00<?php

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Caching\WpCacheEngine;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;

/**
 * Service provider for the object cache mechanism.
 */
class ObjectCacheServiceProvider extends AbstractServiceProvider {

	/**
	 * The classes/interfaces that are serviced by this service provider.
	 *
	 * @var array
	 */
	protected $provides = array(
		WpCacheEngine::class,
	);

	/**
	 * Register the classes.
	 */
	public function register() {
		$this->share( WpCacheEngine::class );
	}
}
Internal/DependencyManagement/AbstractServiceProvider.php000064400000015611152333011040017677 0ustar00<?php
/**
 * AbstractServiceProvider class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement;

use Automattic\WooCommerce\Vendor\League\Container\Argument\RawArgument;
use Automattic\WooCommerce\Vendor\League\Container\Definition\DefinitionInterface;
use Automattic\WooCommerce\Vendor\League\Container\ServiceProvider\AbstractServiceProvider as BaseServiceProvider;

/**
 * Base class for the service providers used to register classes in the container.
 *
 * See the documentation of the original class this one is based on (https://container.thephpleague.com/3.x/service-providers)
 * for basic usage details. What this class adds is:
 *
 * - The `add_with_auto_arguments` method that allows to register classes without having to specify the injection method arguments.
 * - The `share_with_auto_arguments` method, sibling of the above.
 * - Convenience `add` and `share` methods that are just proxies for the same methods in `$this->getContainer()`.
 */
abstract class AbstractServiceProvider extends BaseServiceProvider {

	/**
	 * Register a class in the container and use reflection to guess the injection method arguments.
	 *
	 * WARNING: this method uses reflection, so please have performance in mind when using it.
	 *
	 * @param string $class_name Class name to register.
	 * @param mixed  $concrete   The concrete to register. Can be a shared instance, a factory callback, or a class name.
	 * @param bool   $shared Whether to register the class as shared (`get` always returns the same instance) or not.
	 *
	 * @return DefinitionInterface The generated container definition.
	 *
	 * @throws ContainerException Error when reflecting the class, or class injection method is not public, or an argument has no valid type hint.
	 */
	protected function add_with_auto_arguments( string $class_name, $concrete = null, bool $shared = false ) : DefinitionInterface {
		$definition = new Definition( $class_name, $concrete );

		$function = $this->reflect_class_or_callable( $class_name, $concrete );

		if ( ! is_null( $function ) ) {
			$arguments = $function->getParameters();
			foreach ( $arguments as $argument ) {
				if ( $argument->isDefaultValueAvailable() ) {
					$default_value = $argument->getDefaultValue();
					$definition->addArgument( new RawArgument( $default_value ) );
				} else {
					$argument_class = $this->get_class( $argument );
					if ( is_null( $argument_class ) ) {
						throw new ContainerException( "Argument '{$argument->getName()}' of class '$class_name' doesn't have a type hint or has one that doesn't specify a class." );
					}

					$definition->addArgument( $argument_class->name );
				}
			}
		}

		// Register the definition only after being sure that no exception will be thrown.
		$this->getContainer()->add( $definition->getAlias(), $definition, $shared );

		return $definition;
	}

	/**
	 * Gets the class of a parameter.
	 *
	 * This method is a replacement for ReflectionParameter::getClass,
	 * which is deprecated as of PHP 8.
	 *
	 * @param \ReflectionParameter $parameter The parameter to get the class for.
	 *
	 * @return \ReflectionClass|null The class of the parameter, or null if it hasn't any.
	 */
	private function get_class( \ReflectionParameter $parameter ) {
		return $parameter->getType() && ! $parameter->getType()->isBuiltin()
			? new \ReflectionClass( $parameter->getType()->getName() )
			: null;
	}

	/**
	 * Check if a combination of class name and concrete is valid for registration.
	 * Also return the class injection method if the concrete is either a class name or null (then use the supplied class name).
	 *
	 * @param string $class_name The class name to check.
	 * @param mixed  $concrete   The concrete to check.
	 *
	 * @return \ReflectionFunctionAbstract|null A reflection instance for the $class_name injection method or $concrete injection method or callable; null otherwise.
	 * @throws ContainerException Class has a private injection method, can't reflect class, or the concrete is invalid.
	 */
	private function reflect_class_or_callable( string $class_name, $concrete ) {
		if ( ! isset( $concrete ) || is_string( $concrete ) && class_exists( $concrete ) ) {
			$class = $concrete ?? $class_name;

			if ( ! method_exists( $class, Definition::INJECTION_METHOD ) ) {
				return null;
			}

			$method = new \ReflectionMethod( $class, Definition::INJECTION_METHOD );

			$missing_modifiers = array();
			if ( ! $method->isFinal() ) {
				$missing_modifiers[] = 'final';
			}
			if ( ! $method->isPublic() ) {
				$missing_modifiers[] = 'public';
			}
			if ( ! empty( $missing_modifiers ) ) {
				throw new ContainerException( "Method '" . Definition::INJECTION_METHOD . "' of class '$class' isn't '" . implode( ' ', $missing_modifiers ) . "', instances can't be created." );
			}

			return $method;
		} elseif ( is_callable( $concrete ) ) {
			try {
				return new \ReflectionFunction( $concrete );
			} catch ( \ReflectionException $ex ) {
				throw new ContainerException( "Error when reflecting callable: {$ex->getMessage()}" );
			}
		}

		return null;
	}

	/**
	 * Register a class in the container and use reflection to guess the injection method arguments.
	 * The class is registered as shared, so `get` on the container always returns the same instance.
	 *
	 * WARNING: this method uses reflection, so please have performance in mind when using it.
	 *
	 * @param string $class_name Class name to register.
	 * @param mixed  $concrete   The concrete to register. Can be a shared instance, a factory callback, or a class name.
	 *
	 * @return DefinitionInterface The generated container definition.
	 *
	 * @throws ContainerException Error when reflecting the class, or class injection method is not public, or an argument has no valid type hint.
	 */
	protected function share_with_auto_arguments( string $class_name, $concrete = null ) : DefinitionInterface {
		return $this->add_with_auto_arguments( $class_name, $concrete, true );
	}

	/**
	 * Register an entry in the container.
	 *
	 * @param string     $id Entry id (typically a class or interface name).
	 * @param mixed|null $concrete Concrete entity to register under that id, null for automatic creation.
	 * @param bool|null  $shared Whether to register the class as shared (`get` always returns the same instance) or not.
	 *
	 * @return DefinitionInterface The generated container definition.
	 */
	protected function add( string $id, $concrete = null, bool $shared = null ) : DefinitionInterface {
		return $this->getContainer()->add( $id, $concrete, $shared );
	}

	/**
	 * Register a shared entry in the container (`get` always returns the same instance).
	 *
	 * @param string     $id Entry id (typically a class or interface name).
	 * @param mixed|null $concrete Concrete entity to register under that id, null for automatic creation.
	 *
	 * @return DefinitionInterface The generated container definition.
	 */
	protected function share( string $id, $concrete = null ) : DefinitionInterface {
		return $this->add( $id, $concrete, true );
	}
}
Internal/DependencyManagement/Definition.php000064400000002206152333011040015164 0ustar00<?php
/**
 * An extension to the Definition class to prevent constructor injection from being possible.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement;

use Automattic\WooCommerce\Vendor\League\Container\Definition\Definition as BaseDefinition;

/**
 * An extension of the definition class that replaces constructor injection with method injection.
 */
class Definition extends BaseDefinition {

	/**
	 * The standard method that we use for dependency injection.
	 */
	public const INJECTION_METHOD = 'init';

	/**
	 * Resolve a class using method injection instead of constructor injection.
	 *
	 * @param string $concrete The concrete to instantiate.
	 *
	 * @return object
	 */
	protected function resolveClass( string $concrete ) {
		$resolved = $this->resolveArguments( $this->arguments );
		$concrete = new $concrete();

		// Constructor injection causes backwards compatibility problems
		// so we will rely on method injection via an internal method.
		if ( method_exists( $concrete, static::INJECTION_METHOD ) ) {
			call_user_func_array( array( $concrete, static::INJECTION_METHOD ), $resolved );
		}

		return $concrete;
	}
}
Internal/DependencyManagement/ContainerException.php000064400000001256152333011040016701 0ustar00<?php
/**
 * ExtendedContainer class file.
 */

namespace Automattic\WooCommerce\Internal\DependencyManagement;

/**
 * Class ContainerException.
 * Used to signal error conditions related to the dependency injection container.
 */
class ContainerException extends \Exception {
	/**
	 * Create a new instance of the class.
	 *
	 * @param null            $message The exception message to throw.
	 * @param int             $code The error code.
	 * @param \Exception|null $previous The previous throwable used for exception chaining.
	 */
	public function __construct( $message = null, $code = 0, \Exception $previous = null ) {
		parent::__construct( $message, $code, $previous );
	}
}
Internal/Utilities/Users.php000064400000001460152333011040012076 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Utilities;

/**
 * Helper functions for working with users.
 */
class Users {
	/**
	 * Indicates if the user qualifies as site administrator.
	 *
	 * In the context of multisite networks, this means that they must have the `manage_sites`
	 * capability. In all other cases, they must have the `manage_options` capability.
	 *
	 * @param int $user_id Optional, used to specify a specific user (otherwise we look at the current user).
	 *
	 * @return bool
	 */
	public static function is_site_administrator( int $user_id = 0 ): bool {
		$user = 0 === $user_id ? wp_get_current_user() : get_user_by( 'id', $user_id );

		if ( false === $user ) {
			return false;
		}

		return is_multisite() ? $user->has_cap( 'manage_sites' ) : $user->has_cap( 'manage_options' );
	}
}
Internal/Utilities/URL.php000064400000032143152333011040011441 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Utilities;

/**
 * Provides an easy method of assessing URLs, including filepaths (which will be silently
 * converted to a file:// URL if provided).
 */
class URL {
	/**
	 * Components of the URL being assessed.
	 *
	 * The keys match those potentially returned by the parse_url() function, except
	 * that they are always defined and 'drive' (Windows drive letter) has been added.
	 *
	 * @var string|null[]
	 */
	private $components = array(
		'drive'    => null,
		'fragment' => null,
		'host'     => null,
		'pass'     => null,
		'path'     => null,
		'port'     => null,
		'query'    => null,
		'scheme'   => null,
		'user'     => null,
	);

	/**
	 * If the URL (or filepath) is absolute.
	 *
	 * @var bool
	 */
	private $is_absolute;

	/**
	 * If the URL (or filepath) represents a directory other than the root directory.
	 *
	 * This is useful at different points in the process, when deciding whether to re-apply
	 * a trailing slash at the end of processing or when we need to calculate how many
	 * directory traversals are needed to form a (grand-)parent URL.
	 *
	 * @var bool
	 */
	private $is_non_root_directory;

	/**
	 * The components of the URL's path.
	 *
	 * For instance, in the case of "file:///srv/www/wp.site" (noting that a file URL has
	 * no host component) this would contain:
	 *
	 *     [ "srv", "www", "wp.site" ]
	 *
	 * In the case of a non-file URL such as "https://example.com/foo/bar/baz" (noting the
	 * host is not part of the path) it would contain:
	 *
	 *    [ "foo", "bar", "baz" ]
	 *
	 * @var array
	 */
	private $path_parts = array();

	/**
	 * The URL.
	 *
	 * @var string
	 */
	private $url;

	/**
	 * Creates and processes the provided URL (or filepath).
	 *
	 * @throws URLException If the URL (or filepath) is seriously malformed.
	 *
	 * @param string $url The URL (or filepath).
	 */
	public function __construct( string $url ) {
		$this->url = $url;
		$this->preprocess();
		$this->process_path();
	}

	/**
	 * Makes all slashes forward slashes, converts filepaths to file:// URLs, and
	 * other processing to help with comprehension of filepaths.
	 *
	 * @throws URLException If the URL is seriously malformed.
	 */
	private function preprocess() {
		// For consistency, all slashes should be forward slashes.
		$this->url = str_replace( '\\', '/', $this->url );

		// Windows: capture the drive letter if provided.
		if ( preg_match( '#^(file://)?([a-z]):/(?!/).*#i', $this->url, $matches ) ) {
			$this->components['drive'] = $matches[2];
		}

		/*
		 * If there is no scheme, assume and prepend "file://". An exception is made for cases where the URL simply
		 * starts with exactly two forward slashes, which indicates 'any scheme' (most commonly, that is used when
		 * there is freedom to switch between 'http' and 'https').
		 */
		if ( ! preg_match( '#^[a-z]+://#i', $this->url ) && ! preg_match( '#^//(?!/)#', $this->url ) ) {
			$this->url = 'file://' . $this->url;
		}

		$parsed_components = wp_parse_url( $this->url );

		// If we received a really badly formed URL, let's go no further.
		if ( false === $parsed_components ) {
			throw new URLException(
				sprintf(
				/* translators: %s is the URL. */
					__( '%s is not a valid URL.', 'woocommerce' ),
					$this->url
				)
			);
		}

		$this->components = array_merge( $this->components, $parsed_components );

		// File URLs cannot have a host. However, the initial path segment *or* the Windows drive letter
		// (if present) may be incorrectly be interpreted as the host name.
		if ( 'file' === $this->components['scheme'] && ! empty( $this->components['host'] ) ) {
			// If we do not have a drive letter, then simply merge the host and the path together.
			if ( null === $this->components['drive'] ) {
				$this->components['path'] = $this->components['host'] . ( $this->components['path'] ?? '' );
			}

			// Restore the host to null in this situation.
			$this->components['host'] = null;
		}
	}

	/**
	 * Simplifies the path if possible, by resolving directory traversals to the extent possible
	 * without touching the filesystem.
	 */
	private function process_path() {
		$segments                    = explode( '/', $this->components['path'] );
		$this->is_absolute           = substr( $this->components['path'], 0, 1 ) === '/' || ! empty( $this->components['host'] );
		$this->is_non_root_directory = substr( $this->components['path'], -1, 1 ) === '/' && strlen( $this->components['path'] ) > 1;
		$resolve_traversals          = 'file' !== $this->components['scheme'] || $this->is_absolute;
		$retain_traversals           = false;

		// Clean the path.
		foreach ( $segments as $part ) {
			// Drop empty segments.
			if ( strlen( $part ) === 0 || '.' === $part ) {
				continue;
			}

			// Directory traversals created with percent-encoding syntax should also be detected.
			$is_traversal = str_ireplace( '%2e', '.', $part ) === '..';

			// Resolve directory traversals (if allowed: see further comment relating to this).
			if ( $resolve_traversals && $is_traversal ) {
				if ( count( $this->path_parts ) > 0 && ! $retain_traversals ) {
					$this->path_parts = array_slice( $this->path_parts, 0, count( $this->path_parts ) - 1 );
					continue;
				} elseif ( $this->is_absolute ) {
					continue;
				}
			}

			/*
			 * Consider allowing directory traversals to be resolved (ie, the process that converts 'foo/bar/../baz' to
			 * 'foo/baz').
			 *
			 * 1. For this decision point, we are only concerned with relative filepaths (in all other cases,
			 *    $resolve_traversals will already be true).
			 * 2. This is a 'one time' and unidirectional operation. We only wish to flip from false to true, and we
			 *    never wish to do this more than once.
			 * 3. We only flip the switch after we have examined all leading '..' traversal segments.
			 */
			if ( false === $resolve_traversals && '..' !== $part && 'file' === $this->components['scheme'] && ! $this->is_absolute ) {
				$resolve_traversals = true;
			}

			/*
			 * Set a flag indicating that traversals should be retained. This is done to ensure we don't prematurely
			 * discard traversals at the start of the path.
			 */
			$retain_traversals = $resolve_traversals && '..' === $part;

			// Retain this part of the path.
			$this->path_parts[] = $part;
		}

		// Protect against empty relative paths.
		if ( count( $this->path_parts ) === 0 && ! $this->is_absolute ) {
			$this->path_parts = array( '.' );
			$this->is_non_root_directory = true;
		}

		// Reform the path from the processed segments, appending a leading slash if it is absolute and restoring
		// the Windows drive letter if we have one.
		$this->components['path'] = ( $this->is_absolute ? '/' : '' ) . implode( '/', $this->path_parts ) . ( $this->is_non_root_directory ? '/' : '' );
	}

	/**
	 * Returns the processed URL as a string.
	 *
	 * @return string
	 */
	public function __toString(): string {
		return $this->get_url();
	}

	/**
	 * Returns all possible parent URLs for the current URL.
	 *
	 * @return string[]
	 */
	public function get_all_parent_urls(): array {
		$max_parent = count( $this->path_parts );
		$parents    = array();

		/*
		 * If we are looking at a relative path that begins with at least one traversal (example: "../../foo")
		 * then we should only return one parent URL (otherwise, we'd potentially have to return an infinite
		 * number of parent URLs since we can't know how far the tree extends).
		 */
		if ( $max_parent > 0 && ! $this->is_absolute && '..' === $this->path_parts[0] ) {
			$max_parent = 1;
		}

		for ( $level = 1; $level <= $max_parent; $level++ ) {
			$parents[] = $this->get_parent_url( $level );
		}

		return $parents;
	}

	/**
	 * Outputs the parent URL.
	 *
	 * For example, if $this->get_url() returns "https://example.com/foo/bar/baz" then
	 * this method will return "https://example.com/foo/bar/".
	 *
	 * When a grand-parent is needed, the optional $level parameter can be used. By default
	 * this is set to 1 (parent). 2 will yield the grand-parent, 3 will yield the great
	 * grand-parent, etc.
	 *
	 * If a level is specified that exceeds the number of path segments, this method will
	 * return false.
	 *
	 * @param int $level Used to indicate the level of parent.
	 *
	 * @return string|false
	 */
	public function get_parent_url( int $level = 1 ) {
		if ( $level < 1 ) {
			$level = 1;
		}

		$parts_count               = count( $this->path_parts );
		$parent_path_parts_to_keep = $parts_count - $level;

		/*
		 * With the exception of file URLs, we do not allow obtaining (grand-)parent directories that require
		 * us to describe them using directory traversals. For example, given "http://hostname/foo/bar/baz.png" we do
		 * not permit determining anything more than 2 levels up (we cannot go beyond "http://hostname/").
		 */
		if ( 'file' !== $this->components['scheme'] && $parent_path_parts_to_keep < 0 ) {
			return false;
		}

		// In the specific case of an absolute filepath describing the root directory, there can be no parent.
		if ( 'file' === $this->components['scheme'] && $this->is_absolute && empty( $this->path_parts ) ) {
			return false;
		}

		// Handle cases where the path starts with one or more 'dot segments'. Since the path has already been
		// processed, we can be confident that any such segments are at the start of the path.
		if ( $parts_count > 0 && ( '.' === $this->path_parts[0] || '..' === $this->path_parts[0] ) ) {
			// Determine the index of the last dot segment (ex: given the path '/../../foo' it would be 1).
			$single_dots   = array_keys( $this->path_parts, '.', true );
			$double_dots   = array_keys( $this->path_parts, '..', true );
			$max_dot_index = max( array_merge( $single_dots, $double_dots ) );

			// Prepend the required number of traversals and discard unnessary trailing segments.
			$last_traversal = $max_dot_index + ( $this->is_non_root_directory ? 1 : 0 );
			$parent_path    = str_repeat( '../', $level ) . join( '/', array_slice( $this->path_parts, 0, $last_traversal ) );
		} elseif ( $parent_path_parts_to_keep < 0 ) {
			// For relative filepaths only, we use traversals to describe the requested parent.
			$parent_path = untrailingslashit( str_repeat( '../', $parent_path_parts_to_keep * -1 ) );
		} else {
			// Otherwise, in a very simple case, we just remove existing parts.
			$parent_path = implode( '/', array_slice( $this->path_parts, 0, $parent_path_parts_to_keep ) );
		}

		if ( $this->is_relative() && '' === $parent_path ) {
			$parent_path = '.';
		}

		// Append a trailing slash, since a parent is always a directory. The only exception is the current working directory.
		$parent_path .= '/';

		// For absolute paths, apply a leading slash (does not apply if we have a root path).
		if ( $this->is_absolute && 0 !== strpos( $parent_path, '/' ) ) {
			$parent_path = '/' . $parent_path;
		}

		// Form the parent URL (ditching the query and fragment, if set).
		$parent_url = $this->get_url(
			array(
				'path'     => $parent_path,
				'query'    => null,
				'fragment' => null,
			)
		);

		// We process the parent URL through a fresh instance of this class, for consistency.
		return ( new self( $parent_url ) )->get_url();
	}

	/**
	 * Outputs the processed URL.
	 *
	 * Borrows from https://www.php.net/manual/en/function.parse-url.php#106731
	 *
	 * @param array $component_overrides If provided, these will override values set in $this->components.
	 *
	 * @return string
	 */
	public function get_url( array $component_overrides = array() ): string {
		$components = array_merge( $this->components, $component_overrides );

		$scheme = null !== $components['scheme'] ? $components['scheme'] . '://' : '//';
		$host   = null !== $components['host'] ? $components['host'] : '';
		$port   = null !== $components['port'] ? ':' . $components['port'] : '';
		$path   = $this->get_path( $components['path'] );

		// Special handling for hostless URLs (typically, filepaths) referencing the current working directory.
		if ( '' === $host && ( '' === $path || '.' === $path ) ) {
			$path = './';
		}

		$user      = null !== $components['user'] ? $components['user'] : '';
		$pass      = null !== $components['pass'] ? ':' . $components['pass'] : '';
		$user_pass = ( ! empty( $user ) || ! empty( $pass ) ) ? $user . $pass . '@' : '';

		$query    = null !== $components['query'] ? '?' . $components['query'] : '';
		$fragment = null !== $components['fragment'] ? '#' . $components['fragment'] : '';

		return $scheme . $user_pass . $host . $port . $path . $query . $fragment;
	}

	/**
	 * Outputs the path. Especially useful if it was a a regular filepath that was passed in originally.
	 *
	 * @param string $path_override If provided this will be used as the URL path. Does not impact drive letter.
	 *
	 * @return string
	 */
	public function get_path( string $path_override = null ): string {
		return ( $this->components['drive'] ? $this->components['drive'] . ':' : '' ) . ( $path_override ?? $this->components['path'] );
	}

	/**
	 * Indicates if the URL or filepath was absolute.
	 *
	 * @return bool True if absolute, else false.
	 */
	public function is_absolute(): bool {
		return $this->is_absolute;
	}

	/**
	 * Indicates if the URL or filepath was relative.
	 *
	 * @return bool True if relative, else false.
	 */
	public function is_relative(): bool {
		return ! $this->is_absolute;
	}
}
Internal/Utilities/DatabaseUtil.php000064400000020536152333011040013344 0ustar00<?php
/**
 * DatabaseUtil class file.
 */

namespace Automattic\WooCommerce\Internal\Utilities;

use DateTime;
use DateTimeZone;

/**
 * A class of utilities for dealing with the database.
 */
class DatabaseUtil {

	/**
	 * Wrapper for the WordPress dbDelta function, allows to execute a series of SQL queries.
	 *
	 * @param string $queries The SQL queries to execute.
	 * @param bool   $execute Ture to actually execute the queries, false to only simulate the execution.
	 * @return array The result of the execution (or simulation) from dbDelta.
	 */
	public function dbdelta( string $queries = '', bool $execute = true ): array {
		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		return dbDelta( $queries, $execute );
	}

	/**
	 * Given a set of table creation SQL statements, check which of the tables are currently missing in the database.
	 *
	 * @param string $creation_queries The SQL queries to execute ("CREATE TABLE" statements, same format as for dbDelta).
	 * @return array An array containing the names of the tables that currently don't exist in the database.
	 */
	public function get_missing_tables( string $creation_queries ): array {
		$dbdelta_output = $this->dbdelta( $creation_queries, false );
		$parsed_output  = $this->parse_dbdelta_output( $dbdelta_output );
		return $parsed_output['created_tables'];
	}

	/**
	 * Parses the output given by dbdelta and returns information about it.
	 *
	 * @param array $dbdelta_output The output from the execution of dbdelta.
	 * @return array[] An array containing a 'created_tables' key whose value is an array with the names of the tables that have been (or would have been) created.
	 */
	public function parse_dbdelta_output( array $dbdelta_output ): array {
		$created_tables = array();

		foreach ( $dbdelta_output as $table_name => $result ) {
			if ( "Created table $table_name" === $result ) {
				$created_tables[] = $table_name;
			}
		}

		return array( 'created_tables' => $created_tables );
	}

	/**
	 * Drops a database table.
	 *
	 * @param string $table_name The name of the table to drop.
	 * @param bool   $add_prefix True if the table name passed needs to be prefixed with $wpdb->prefix before processing.
	 * @return bool True on success, false on error.
	 */
	public function drop_database_table( string $table_name, bool $add_prefix = false ) {
		global $wpdb;

		if ( $add_prefix ) {
			$table_name = $wpdb->prefix . $table_name;
		}

		//phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return $wpdb->query( "DROP TABLE IF EXISTS `{$table_name}`" );
	}

	/**
	 * Drops a table index, if both the table and the index exist.
	 *
	 * @param string $table_name The name of the table that contains the index.
	 * @param string $index_name The name of the index to be dropped.
	 * @return bool True if the index has been dropped, false if either the table or the index don't exist.
	 */
	public function drop_table_index( string $table_name, string $index_name ): bool {
		global $wpdb;

		if ( empty( $this->get_index_columns( $table_name, $index_name ) ) ) {
			return false;
		}

		// phpcs:ignore WordPress.DB.PreparedSQL
		$wpdb->query( "ALTER TABLE $table_name DROP INDEX $index_name" );
		return true;
	}

	/**
	 * Create a primary key for a table, only if the table doesn't have a primary key already.
	 *
	 * @param string $table_name Table name.
	 * @param array  $columns An array with the index column names.
	 * @return bool True if the key has been created, false if the table already had a primary key.
	 */
	public function create_primary_key( string $table_name, array $columns ) {
		global $wpdb;

		if ( ! empty( $this->get_index_columns( $table_name ) ) ) {
			return false;
		}

		// phpcs:ignore WordPress.DB.PreparedSQL
		$wpdb->query( "ALTER TABLE $table_name ADD PRIMARY KEY(`" . join( '`,`', $columns ) . '`)' );
		return true;
	}

	/**
	 * Get the columns of a given table index, or of the primary key.
	 *
	 * @param string $table_name Table name.
	 * @param string $index_name Index name, empty string for the primary key.
	 * @return array The index columns. Empty array if the table or the index don't exist.
	 */
	public function get_index_columns( string $table_name, string $index_name = '' ): array {
		global $wpdb;

		if ( empty( $index_name ) ) {
			$index_name = 'PRIMARY';
		}

		// phpcs:disable WordPress.DB.PreparedSQL
		return $wpdb->get_col(
			"
SELECT column_name FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_name='$table_name'
AND table_schema='" . DB_NAME . "'
AND index_name='$index_name'"
		);
		// phpcs:enable WordPress.DB.PreparedSQL
	}

	/**
	 * Formats an object value of type `$type` for inclusion in the database.
	 *
	 * @param mixed  $value Raw value.
	 * @param string $type  Data type.
	 * @return mixed
	 * @throws \Exception When an invalid type is passed.
	 */
	public function format_object_value_for_db( $value, string $type ) {
		switch ( $type ) {
			case 'decimal':
				$value = wc_format_decimal( $value, false, true );
				break;
			case 'int':
				$value = (int) $value;
				break;
			case 'bool':
				$value = wc_string_to_bool( $value );
				break;
			case 'string':
				$value = strval( $value );
				break;
			case 'date':
				// Date properties are converted to the WP timezone (see WC_Data::set_date_prop() method), however
				// for our own tables we persist dates in GMT.
				$value = $value ? ( new DateTime( $value ) )->setTimezone( new DateTimeZone( '+00:00' ) )->format( 'Y-m-d H:i:s' ) : null;
				break;
			case 'date_epoch':
				$value = $value ? ( new DateTime( "@{$value}" ) )->format( 'Y-m-d H:i:s' ) : null;
				break;
			default:
				throw new \Exception( 'Invalid type received: ' . $type );
		}

		return $value;
	}

	/**
	 * Returns the `$wpdb` placeholder to use for data type `$type`.
	 *
	 * @param string $type Data type.
	 * @return string
	 * @throws \Exception When an invalid type is passed.
	 */
	public function get_wpdb_format_for_type( string $type ) {
		static $wpdb_placeholder_for_type = array(
			'int'        => '%d',
			'decimal'    => '%f',
			'string'     => '%s',
			'date'       => '%s',
			'date_epoch' => '%s',
			'bool'       => '%d',
		);

		if ( ! isset( $wpdb_placeholder_for_type[ $type ] ) ) {
			throw new \Exception( 'Invalid column type: ' . $type );
		}

		return $wpdb_placeholder_for_type[ $type ];
	}

	/**
	 * Generates ON DUPLICATE KEY UPDATE clause to be used in migration.
	 *
	 * @param array $columns List of column names.
	 *
	 * @return string SQL clause for INSERT...ON DUPLICATE KEY UPDATE
	 */
	public function generate_on_duplicate_statement_clause( array $columns ): string {
		$update_value_statements = array();
		foreach ( $columns as $column ) {
			$update_value_statements[] = "`$column` = VALUES( `$column` )";
		}
		$update_value_clause = implode( ', ', $update_value_statements );

		return "ON DUPLICATE KEY UPDATE $update_value_clause";
	}

	/**
	 * Hybrid of $wpdb->update and $wpdb->insert. It will try to update a row, and if it doesn't exist, it will insert it. This needs unique constraints to be set on the table on all ID columns.
	 *
	 * You can use this function only when:
	 * 1. There is only one unique constraint on the table. The constraint can contain multiple columns, but it must be the only one unique constraint.
	 * 2. The complete unique constraint must be part of the $data array.
	 * 3. You do not need the LAST_INSERT_ID() value.
	 *
	 * @param string $table_name Table name.
	 * @param array  $data Unescaped data to update (in column => value pairs).
	 * @param array  $format An array of formats to be mapped to each of the values in $data.
	 *
	 * @return int Returns the value of DB's  ON DUPLICATE KEY UPDATE clause.
	 */
	public function insert_on_duplicate_key_update( $table_name, $data, $format ) : int {
		global $wpdb;

		$columns             = array_keys( $data );
		$column_clause       = '`' . implode( '`, `', $columns ) . '`';
		$value_placeholders  = implode( ', ', array_values( $format ) );
		$on_duplicate_clause = $this->generate_on_duplicate_statement_clause( $columns );
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Values are escaped in $wpdb->prepare.
		$sql = $wpdb->prepare(
			"
INSERT INTO $table_name ( $column_clause )
VALUES ( $value_placeholders )
$on_duplicate_clause
",
			array_values( $data )
		);
		// phpcs:enable
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is prepared.
		return $wpdb->query( $sql );
	}

}
Internal/Utilities/COTMigrationUtil.php000064400000012454152333011040014137 0ustar00<?php
/**
 * Utility functions meant for helping in migration from posts tables to custom order tables.
 */

namespace Automattic\WooCommerce\Internal\Utilities;

use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use WC_Order;
use WP_Post;

/**
 * Utility functions meant for helping in migration from posts tables to custom order tables.
 */
class COTMigrationUtil {

	/**
	 * Custom order table controller.
	 *
	 * @var CustomOrdersTableController
	 */
	private $table_controller;

	/**
	 * Data synchronizer.
	 *
	 * @var DataSynchronizer
	 */
	private $data_synchronizer;

	/**
	 * Initialize method, invoked by the DI container.
	 *
	 * @internal Automatically called by the container.
	 * @param CustomOrdersTableController $table_controller Custom order table controller.
	 * @param DataSynchronizer            $data_synchronizer Data synchronizer.
	 *
	 * @return void
	 */
	final public function init( CustomOrdersTableController $table_controller, DataSynchronizer $data_synchronizer ) {
		$this->table_controller  = $table_controller;
		$this->data_synchronizer = $data_synchronizer;
	}

	/**
	 * Helper function to get screen name of orders page in wp-admin.
	 *
	 * @throws \Exception If called from outside of wp-admin.
	 *
	 * @return string
	 */
	public function get_order_admin_screen() : string {
		if ( ! is_admin() ) {
			throw new \Exception( 'This function should only be called in admin.' );
		}
		return $this->custom_orders_table_usage_is_enabled() && function_exists( 'wc_get_page_screen_id' )
			? wc_get_page_screen_id( 'shop-order' )
			: 'shop_order';
	}

	/**
	 * Helper function to get whether custom order tables are enabled or not.
	 *
	 * @return bool
	 */
	private function custom_orders_table_usage_is_enabled() : bool {
		return $this->table_controller->custom_orders_table_usage_is_enabled();
	}

	/**
	 * Checks if posts and order custom table sync is enabled and there are no pending orders.
	 *
	 * @return bool
	 */
	public function is_custom_order_tables_in_sync() : bool {
		$sync_status = $this->data_synchronizer->get_sync_status();
		return 0 === $sync_status['current_pending_count'] && $this->data_synchronizer->data_sync_is_enabled();
	}

	/**
	 * Gets value of a meta key from WC_Data object if passed, otherwise from the post object.
	 * This helper function support backward compatibility for meta box functions, when moving from posts based store to custom tables.
	 *
	 * @param WP_Post|null  $post Post object, meta will be fetched from this only when `$data` is not passed.
	 * @param \WC_Data|null $data WC_Data object, will be preferred over post object when passed.
	 * @param string        $key Key to fetch metadata for.
	 * @param bool          $single Whether metadata is single.
	 *
	 * @return array|mixed|string Value of the meta key.
	 */
	public function get_post_or_object_meta( ?WP_Post $post, ?\WC_Data $data, string $key, bool $single ) {
		if ( isset( $data ) ) {
			if ( method_exists( $data, "get$key" ) ) {
				return $data->{"get$key"}();
			}
			return $data->get_meta( $key, $single );
		} else {
			return get_post_meta( $post->ID, $key, $single );
		}
	}

	/**
	 * Helper function to initialize the global $theorder object, mostly used during order meta boxes rendering.
	 *
	 * @param WC_Order|WP_Post $post_or_order_object Post or order object.
	 *
	 * @return bool|WC_Order|WC_Order_Refund WC_Order object.
	 */
	public function init_theorder_object( $post_or_order_object ) {
		global $theorder;
		if ( $theorder instanceof WC_Order ) {
			return $theorder;
		}

		if ( $post_or_order_object instanceof WC_Order ) {
			$theorder = $post_or_order_object;
		} else {
			$theorder = wc_get_order( $post_or_order_object->ID );
		}
		return $theorder;
	}

	/**
	 * Helper function to get ID from a post or order object.
	 *
	 * @param WP_Post/WC_Order $post_or_order_object WP_Post/WC_Order object to get ID for.
	 *
	 * @return int Order or post ID.
	 */
	public function get_post_or_order_id( $post_or_order_object ) : int {
		if ( is_numeric( $post_or_order_object ) ) {
			return (int) $post_or_order_object;
		} elseif ( $post_or_order_object instanceof WC_Order ) {
			return $post_or_order_object->get_id();
		} elseif ( $post_or_order_object instanceof WP_Post ) {
			return $post_or_order_object->ID;
		}
		return 0;
	}

	/**
	 * Checks if passed id, post or order object is a WC_Order object.
	 *
	 * @param int|WP_Post|WC_Order $order_id Order ID, post object or order object.
	 * @param string[]             $types    Types to match against.
	 *
	 * @return bool Whether the passed param is an order.
	 */
	public function is_order( $order_id, array $types = array( 'shop_order' ) ) : bool {
		$order_id         = $this->get_post_or_order_id( $order_id );
		$order_data_store = \WC_Data_Store::load( 'order' );
		return in_array( $order_data_store->get_order_type( $order_id ), $types, true );
	}

	/**
	 * Returns type pf passed id, post or order object.
	 *
	 * @param int|WP_Post|WC_Order $order_id Order ID, post object or order object.
	 *
	 * @return string|null Type of the order.
	 */
	public function get_order_type( $order_id ) {
		$order_id         = $this->get_post_or_order_id( $order_id );
		$order_data_store = \WC_Data_Store::load( 'order' );
		return $order_data_store->get_order_type( $order_id );
	}
}
Internal/Utilities/URLException.php000064400000000277152333011040013323 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Utilities;

use Exception;

/**
 * Used to represent a problem encountered when processing a URL.
 */
class URLException extends Exception {}
Internal/Utilities/BlocksUtil.php000064400000004275152333011040013057 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Utilities;

/**
 * Helper functions for working with blocks.
 */
class BlocksUtil {

	/**
	 * Return blocks with their inner blocks flattened.
	 *
	 * @param array $blocks Array of blocks as returned by parse_blocks().
	 * @return array All blocks.
	 */
	public static function flatten_blocks( $blocks ) {
		return array_reduce(
			$blocks,
			function( $carry, $block ) {
				array_push( $carry, array_diff_key( $block, array_flip( array( 'innerBlocks' ) ) ) );
				if ( isset( $block['innerBlocks'] ) ) {
					$inner_blocks = self::flatten_blocks( $block['innerBlocks'] );
					return array_merge( $carry, $inner_blocks );
				}

				return $carry;
			},
			array()
		);
	}

	/**
	 * Get all instances of the specified block from the widget area.
	 *
	 * @param array $block_name The name (id) of a block, e.g. `woocommerce/mini-cart`.
	 * @return array Array of blocks as returned by parse_blocks().
	 */
	public static function get_blocks_from_widget_area( $block_name ) {
		return array_reduce(
			get_option( 'widget_block' ),
			function ( $acc, $block ) use ( $block_name ) {
				$parsed_blocks = ! empty( $block ) && is_array( $block ) ? parse_blocks( $block['content'] ) : array();
				if ( ! empty( $parsed_blocks ) && $block_name === $parsed_blocks[0]['blockName'] ) {
					array_push( $acc, $parsed_blocks[0] );
					return $acc;
				}
				return $acc;
			},
			array()
		);
	}

	/**
	 * Get all instances of the specified block on a specific template part.
	 *
	 * @param string $block_name The name (id) of a block, e.g. `woocommerce/mini-cart`.
	 * @param string $template_part_slug The woo page to search, e.g. `header`.
	 * @return array Array of blocks as returned by parse_blocks().
	 */
	public static function get_block_from_template_part( $block_name, $template_part_slug ) {
		$template = get_block_template( get_stylesheet() . '//' . $template_part_slug, 'wp_template_part' );
		$blocks   = parse_blocks( $template->content );

		$flatten_blocks = self::flatten_blocks( $blocks );

		return array_values(
			array_filter(
				$flatten_blocks,
				function ( $block ) use ( $block_name ) {
					return ( $block_name === $block['blockName'] );
				}
			)
		);
	}
}
Internal/Utilities/HtmlSanitizer.php000064400000005321152333011040013572 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Utilities;

/**
 * Utility for re-using WP Kses-based sanitization rules.
 */
class HtmlSanitizer {
	/**
	 * Rules for allowing minimal HTML (breaks, images, paragraphs and spans) without any links.
	 */
	public const LOW_HTML_BALANCED_TAGS_NO_LINKS = array(
		'pre_processors'  => array(
			'stripslashes',
			'force_balance_tags',
		),
		'wp_kses_rules'   => array(
			'br'   => true,
			'img'  => array(
				'alt'   => true,
				'class' => true,
				'src'   => true,
				'title' => true,
			),
			'p'    => array(
				'class' => true,
			),
			'span' => array(
				'class' => true,
				'title' => true,
			),
		),
	);

	/**
	 * Sanitizes the HTML according to the provided rules.
	 *
	 * @see wp_kses()
	 *
	 * @param string $html HTML string to be sanitized.
	 * @param array  $sanitizer_rules {
	 *     Optional and defaults to self::TRIMMED_BALANCED_LOW_HTML_NO_LINKS. Otherwise, one or more of the following
	 *     keys should be set.
	 *
	 *     @type array $pre_processors  Callbacks to run before invoking `wp_kses()`.
	 *     @type array $wp_kses_rules   Element names and attributes to allow, per `wp_kses()`.
	 * }
	 *
	 * @return string
	 */
	public function sanitize( string $html, array $sanitizer_rules = self::LOW_HTML_BALANCED_TAGS_NO_LINKS ): string {
		if ( isset( $sanitizer_rules['pre_processors'] ) && is_array( $sanitizer_rules['pre_processors'] ) ) {
			$html = $this->apply_string_callbacks( $sanitizer_rules['pre_processors'], $html );
		}

		// If no KSES rules are specified, assume all HTML should be stripped.
		$kses_rules = isset( $sanitizer_rules['wp_kses_rules'] ) && is_array( $sanitizer_rules['wp_kses_rules'] )
			? $sanitizer_rules['wp_kses_rules']
			: array();

		return wp_kses( $html, $kses_rules );
	}

	/**
	 * Applies callbacks used to process the string before and after wp_kses().
	 *
	 * If a callback is invalid we will short-circuit and return an empty string, on the grounds that it is better to
	 * output nothing than risky HTML. We also call the problem out via _doing_it_wrong() to highlight the problem (and
	 * increase the chances of this being caught during development).
	 *
	 * @param callable[] $callbacks The callbacks used to mutate the string.
	 * @param string     $string    The string being processed.
	 *
	 * @return string
	 */
	private function apply_string_callbacks( array $callbacks, string $string ): string {
		foreach ( $callbacks as $callback ) {
			if ( ! is_callable( $callback ) ) {
				_doing_it_wrong( __CLASS__ . '::apply', esc_html__( 'String processors must be an array of valid callbacks.', 'woocommerce' ), esc_html( WC()->version ) );
				return '';
			}

			$string = (string) $callback( $string );
		}

		return $string;
	}
}
Internal/Settings/OptionSanitizer.php000064400000003134152333011040013763 0ustar00<?php
/**
 * FormatValidator class.
 */

namespace Automattic\WooCommerce\Internal\Settings;

use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;

defined( 'ABSPATH' ) || exit;

/**
 * This class handles sanitization of core options that need to conform to certain format.
 *
 * @since 6.6.0
 */
class OptionSanitizer {

	use AccessiblePrivateMethods;

	/**
	 * OptionSanitizer constructor.
	 */
	public function __construct() {
		// Sanitize color options.
		$color_options = array(
			'woocommerce_email_base_color',
			'woocommerce_email_background_color',
			'woocommerce_email_body_background_color',
			'woocommerce_email_text_color',
		);

		foreach ( $color_options as $option_name ) {
			self::add_filter(
				"woocommerce_admin_settings_sanitize_option_{$option_name}",
				array( $this, 'sanitize_color_option' ),
				10,
				2
			);
		}
	}

	/**
	 * Sanitizes values for options of type 'color' before persisting to the database.
	 * Falls back to previous/default value for the option if given an invalid value.
	 *
	 * @since 6.6.0
	 * @param string $value Option value.
	 * @param array  $option Option data.
	 * @return string Color in hex format.
	 */
	private function sanitize_color_option( $value, $option ) {
		$value = sanitize_hex_color( $value );

		// If invalid, try the current value.
		if ( ! $value && ! empty( $option['id'] ) ) {
			$value = sanitize_hex_color( get_option( $option['id'] ) );
		}

		// If still invalid, try the default.
		if ( ! $value && ! empty( $option['default'] ) ) {
			$value = sanitize_hex_color( $option['default'] );
		}

		return (string) $value;
	}
}
Internal/BatchProcessing/BatchProcessorInterface.php000064400000005765152333011040016656 0ustar00<?php
/**
 * Interface for batch data processors. See the BatchProcessingController class for usage details.
 */

namespace Automattic\WooCommerce\Internal\BatchProcessing;

/**
 * Interface BatchProcessorInterface
 *
 * @package Automattic\WooCommerce\DataBase
 */
interface BatchProcessorInterface {

	/**
	 * Get a user-friendly name for this processor.
	 *
	 * @return string Name of the processor.
	 */
	public function get_name() : string;

	/**
	 * Get a user-friendly description for this processor.
	 *
	 * @return string Description of what this processor does.
	 */
	public function get_description() : string;

	/**
	 * Get the total number of pending items that require processing.
	 * Once an item is successfully processed by 'process_batch' it shouldn't be included in this count.
	 *
	 * Note that the once the processor is enqueued the batch processor controller will keep
	 * invoking `get_next_batch_to_process` and `process_batch` repeatedly until this method returns zero.
	 *
	 * @return int Number of items pending processing.
	 */
	public function get_total_pending_count() : int;

	/**
	 * Returns the next batch of items that need to be processed.
	 *
	 * A batch item can be anything needed to identify the actual processing to be done,
	 * but whenever possible items should be numbers (e.g. database record ids)
	 * or at least strings, to ease troubleshooting and logging in case of problems.
	 *
	 * The size of the batch returned can be less than $size if there aren't that
	 * many items pending processing (and it can be zero if there isn't anything to process),
	 * but the size should always be consistent with what 'get_total_pending_count' returns
	 * (i.e. the size of the returned batch shouldn't be larger than the pending items count).
	 *
	 * @param int $size Maximum size of the batch to be returned.
	 *
	 * @return array Batch of items to process, containing $size or less items.
	 */
	public function get_next_batch_to_process( int $size ) : array;

	/**
	 * Process data for the supplied batch.
	 *
	 * This method should be prepared to receive items that don't actually need processing
	 * (because they have been processed before) and ignore them, but if at least
	 * one of the batch items that actually need processing can't be processed, an exception should be thrown.
	 *
	 * Once an item has been processed it shouldn't be counted in 'get_total_pending_count'
	 * nor included in 'get_next_batch_to_process' anymore (unless something happens that causes it
	 * to actually require further processing).
	 *
	 * @throw \Exception Something went wrong while processing the batch.
	 *
	 * @param array $batch Batch to process, as returned by 'get_next_batch_to_process'.
	 */
	public function process_batch( array $batch ): void;

	/**
	 * Default (preferred) batch size to pass to 'get_next_batch_to_process'.
	 * The controller will pass this size unless it's externally configured
	 * to use a different size.
	 *
	 * @return int Default batch size.
	 */
	public function get_default_batch_size() : int;
}
Internal/BatchProcessing/BatchProcessingController.php000064400000036525152333011040017234 0ustar00<?php
/**
 * This class is a helper intended to handle data processings that need to happen in batches in a deferred way.
 * It abstracts away the nuances of (re)scheduling actions and dealing with errors.
 *
 * Usage:
 *
 * 1. Create a class that implements BatchProcessorInterface.
 *    The class must either be registered in the dependency injection container, or have a public parameterless constructor,
 *    or an instance must be provided via the 'woocommerce_get_batch_processor' filter.
 * 2. Whenever there's data to be processed invoke the 'enqueue_processor' method in this class,
 *    passing the class name of the processor.
 *
 * That's it, processing will be performed in batches inside scheduled actions; enqueued processors will only
 * be dequeued once they notify that no more items are left to process (or when `force_clear_all_processes` is invoked).
 * Failed batches will be retried after a while.
 *
 * There are also a few public methods to get the list of currently enqueued processors
 * and to check if a given processor is enqueued/actually scheduled.
 */

namespace Automattic\WooCommerce\Internal\BatchProcessing;

/**
 * Class BatchProcessingController
 *
 * @package Automattic\WooCommerce\Internal\Updates.
 */
class BatchProcessingController {
	/*
	 * Identifier of a "watchdog" action that will schedule a processing action
	 * for any processor that is enqueued but not yet scheduled
	 * (because it's been just enqueued or because it threw an error while processing a batch),
	 * that's one single action that reschedules itself continuously.
	 */
	const WATCHDOG_ACTION_NAME = 'wc_schedule_pending_batch_processes';

	/*
	 * Identifier of the action that will do the actual batch processing.
	 * There's one action per enqueued processor that will keep rescheduling itself
	 * as long as there are still pending items to process
	 * (except if there's an error that caused no items to be processed at all).
	 */
	const PROCESS_SINGLE_BATCH_ACTION_NAME = 'wc_run_batch_process';

	const ENQUEUED_PROCESSORS_OPTION_NAME = 'wc_pending_batch_processes';
	const ACTION_GROUP                    = 'wc_batch_processes';

	/**
	 * Instance of WC_Logger class.
	 *
	 * @var \WC_Logger_Interface
	 */
	private $logger;

	/**
	 * BatchProcessingController constructor.
	 *
	 * Schedules the necessary actions to process batches.
	 */
	public function __construct() {
		add_action(
			self::WATCHDOG_ACTION_NAME,
			function () {
				$this->handle_watchdog_action();
			}
		);

		add_action(
			self::PROCESS_SINGLE_BATCH_ACTION_NAME,
			function ( $batch_process ) {
				$this->process_next_batch_for_single_processor( $batch_process );
			},
			10,
			2
		);

		$this->logger = wc_get_logger();
	}

	/**
	 * Enqueue a processor so that it will get batch processing requests from within scheduled actions.
	 *
	 * @param string $processor_class_name Fully qualified class name of the processor, must implement `BatchProcessorInterface`.
	 */
	public function enqueue_processor( string $processor_class_name ): void {
		$pending_updates = $this->get_enqueued_processors();
		if ( ! in_array( $processor_class_name, array_keys( $pending_updates ), true ) ) {
			$pending_updates[] = $processor_class_name;
			$this->set_enqueued_processors( $pending_updates );
		}
		$this->schedule_watchdog_action( false, true );
	}

	/**
	 * Schedule the watchdog action.
	 *
	 * @param bool $with_delay Whether to delay the action execution. Should be true when rescheduling, false when enqueueing.
	 * @param bool $unique     Whether to make the action unique.
	 */
	private function schedule_watchdog_action( bool $with_delay = false, bool $unique = false ): void {
		$time = $with_delay ? time() + HOUR_IN_SECONDS : time();
		as_schedule_single_action(
			$time,
			self::WATCHDOG_ACTION_NAME,
			array(),
			self::ACTION_GROUP,
			$unique
		);
	}

	/**
	 * Schedule a processing action for all the processors that are enqueued but not scheduled
	 * (because they have just been enqueued, or because the processing for a batch failed).
	 */
	private function handle_watchdog_action(): void {
		$pending_processes = $this->get_enqueued_processors();
		if ( empty( $pending_processes ) ) {
			return;
		}
		foreach ( $pending_processes as $process_name ) {
			if ( ! $this->is_scheduled( $process_name ) ) {
				$this->schedule_batch_processing( $process_name );
			}
		}
		$this->schedule_watchdog_action( true );
	}

	/**
	 * Process a batch for a single processor, and handle any required rescheduling or state cleanup.
	 *
	 * @param string $processor_class_name Fully qualified class name of the processor.
	 *
	 * @throws \Exception If error occurred during batch processing.
	 */
	private function process_next_batch_for_single_processor( string $processor_class_name ): void {
		if ( ! $this->is_enqueued( $processor_class_name ) ) {
			return;
		}

		$batch_processor      = $this->get_processor_instance( $processor_class_name );
		$pending_count_before = $batch_processor->get_total_pending_count();
		$error                = $this->process_next_batch_for_single_processor_core( $batch_processor );
		$pending_count_after  = $batch_processor->get_total_pending_count();
		if ( ( $error instanceof \Exception ) && $pending_count_before === $pending_count_after ) {
			// The batch processing failed and no items were processed:
			// reschedule the processing with a delay, and also throw the error
			// so Action Scheduler will ignore the rescheduling if this happens repeatedly.
			$this->schedule_batch_processing( $processor_class_name, true );
			throw $error;
		}
		if ( $pending_count_after > 0 ) {
			$this->schedule_batch_processing( $processor_class_name );
		} else {
			$this->dequeue_processor( $processor_class_name );
		}
	}

	/**
	 * Process a batch for a single processor, updating state and logging any error.
	 *
	 * @param BatchProcessorInterface $batch_processor Batch processor instance.
	 *
	 * @return null|\Exception Exception if error occurred, null otherwise.
	 */
	private function process_next_batch_for_single_processor_core( BatchProcessorInterface $batch_processor ): ?\Exception {
		$details    = $this->get_process_details( $batch_processor );
		$time_start = microtime( true );
		$batch      = $batch_processor->get_next_batch_to_process( $details['current_batch_size'] );
		if ( empty( $batch ) ) {
			return null;
		}
		try {
			$batch_processor->process_batch( $batch );
			$time_taken = microtime( true ) - $time_start;
			$this->update_processor_state( $batch_processor, $time_taken );
		} catch ( \Exception $exception ) {
			$time_taken = microtime( true ) - $time_start;
			$this->log_error( $exception, $batch_processor, $batch );
			$this->update_processor_state( $batch_processor, $time_taken, $exception );
			return $exception;
		}
		return null;
	}

	/**
	 * Get the current state for a given enqueued processor.
	 *
	 * @param BatchProcessorInterface $batch_processor Batch processor instance.
	 *
	 * @return array Current state for the processor, or a "blank" state if none exists yet.
	 */
	private function get_process_details( BatchProcessorInterface $batch_processor ): array {
		return get_option(
			$this->get_processor_state_option_name( $batch_processor ),
			array(
				'total_time_spent'   => 0,
				'current_batch_size' => $batch_processor->get_default_batch_size(),
				'last_error'         => null,
			)
		);
	}

	/**
	 * Get the name of the option where we will be saving state for a given processor.
	 *
	 * @param BatchProcessorInterface $batch_processor Batch processor instance.
	 *
	 * @return string Option name.
	 */
	private function get_processor_state_option_name( BatchProcessorInterface $batch_processor ): string {
		$class_name = get_class( $batch_processor );
		$class_md5  = md5( $class_name );
		// truncate the class name so we know that it will fit in the option name column along with md5 hash and prefix.
		$class_name = substr( $class_name, 0, 140 );
		return 'wc_batch_' . $class_name . '_' . $class_md5;
	}

	/**
	 * Update the state for a processor after a batch has completed processing.
	 *
	 * @param BatchProcessorInterface $batch_processor Batch processor instance.
	 * @param float                   $time_taken Time take by the batch to complete processing.
	 * @param \Exception|null         $last_error Exception object in processing the batch, if there was one.
	 */
	private function update_processor_state( BatchProcessorInterface $batch_processor, float $time_taken, \Exception $last_error = null ): void {
		$current_status                      = $this->get_process_details( $batch_processor );
		$current_status['total_time_spent'] += $time_taken;
		$current_status['last_error']        = null !== $last_error ? $last_error->getMessage() : null;
		update_option( $this->get_processor_state_option_name( $batch_processor ), $current_status, false );
	}

	/**
	 * Schedule a processing action for a single processor.
	 *
	 * @param string $processor_class_name Fully qualified class name of the processor.
	 * @param bool   $with_delay   Whether to schedule the action for immediate execution or for later.
	 */
	private function schedule_batch_processing( string $processor_class_name, bool $with_delay = false ) : void {
		$time = $with_delay ? time() + MINUTE_IN_SECONDS : time();
		as_schedule_single_action( $time, self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
	}

	/**
	 * Check if a batch processing action is already scheduled for a given processor.
	 * Differs from `as_has_scheduled_action` in that this excludes actions in progress.
	 *
	 * @param string $processor_class_name Fully qualified class name of the batch processor.
	 *
	 * @return bool True if a batch processing action is already scheduled for the processor.
	 */
	public function is_scheduled( string $processor_class_name ): bool {
		return as_has_scheduled_action( self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
	}

	/**
	 * Get an instance of a processor given its class name.
	 *
	 * @param string $processor_class_name Full class name of the batch processor.
	 *
	 * @return BatchProcessorInterface Instance of batch processor for the given class.
	 * @throws \Exception If it's not possible to get an instance of the class.
	 */
	private function get_processor_instance( string $processor_class_name ) : BatchProcessorInterface {
		$processor = wc_get_container()->get( $processor_class_name );

		/**
		 * Filters the instance of a processor for a given class name.
		 *
		 * @param object|null $processor The processor instance given by the dependency injection container, or null if none was obtained.
		 * @param string $processor_class_name The full class name of the processor.
		 * @return BatchProcessorInterface|null The actual processor instance to use, or null if none could be retrieved.
		 *
		 * @since 6.8.0.
		 */
		$processor = apply_filters( 'woocommerce_get_batch_processor', $processor, $processor_class_name );
		if ( ! isset( $processor ) && class_exists( $processor_class_name ) ) {
			// This is a fallback for when the batch processor is not registered in the container.
			$processor = new $processor_class_name();
		}
		if ( ! is_a( $processor, BatchProcessorInterface::class ) ) {
			throw new \Exception( "Unable to initialize batch processor instance for $processor_class_name" );
		}
		return $processor;
	}

	/**
	 * Helper method to get list of all the enqueued processors.
	 *
	 * @return array List (of string) of the class names of the enqueued processors.
	 */
	public function get_enqueued_processors() : array {
		return get_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array() );
	}

	/**
	 * Dequeue a processor once it has no more items pending processing.
	 *
	 * @param string $processor_class_name Full processor class name.
	 */
	private function dequeue_processor( string $processor_class_name ): void {
		$pending_processes = $this->get_enqueued_processors();
		if ( in_array( $processor_class_name, $pending_processes, true ) ) {
			$pending_processes = array_diff( $pending_processes, array( $processor_class_name ) );
			$this->set_enqueued_processors( $pending_processes );
		}
	}

	/**
	 * Helper method to set the enqueued processor class names.
	 *
	 * @param array $processors List of full processor class names.
	 */
	private function set_enqueued_processors( array $processors ): void {
		update_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, $processors, false );
	}

	/**
	 * Check if a particular processor is enqueued.
	 *
	 * @param string $processor_class_name Fully qualified class name of the processor.
	 *
	 * @return bool True if the processor is enqueued.
	 */
	public function is_enqueued( string $processor_class_name ) : bool {
		return in_array( $processor_class_name, $this->get_enqueued_processors(), true );
	}

	/**
	 * Dequeue and de-schedule a processor instance so that it won't be processed anymore.
	 *
	 * @param string $processor_class_name Fully qualified class name of the processor.
	 * @return bool True if the processor has been dequeued, false if the processor wasn't enqueued (so nothing has been done).
	 */
	public function remove_processor( string $processor_class_name ): bool {
		$enqueued_processors = $this->get_enqueued_processors();
		if ( ! in_array( $processor_class_name, $enqueued_processors, true ) ) {
			return false;
		}

		$enqueued_processors = array_diff( $enqueued_processors, array( $processor_class_name ) );
		if ( empty( $enqueued_processors ) ) {
			$this->force_clear_all_processes();
		} else {
			update_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, $enqueued_processors, false );
			as_unschedule_all_actions( self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
		}

		return true;
	}

	/**
	 * Dequeues and de-schedules all the processors.
	 */
	public function force_clear_all_processes(): void {
		as_unschedule_all_actions( self::PROCESS_SINGLE_BATCH_ACTION_NAME );
		as_unschedule_all_actions( self::WATCHDOG_ACTION_NAME );
		update_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array(), false );
	}

	/**
	 * Log an error that happened while processing a batch.
	 *
	 * @param \Exception              $error Exception object to log.
	 * @param BatchProcessorInterface $batch_processor Batch processor instance.
	 * @param array                   $batch Batch that was being processed.
	 */
	protected function log_error( \Exception $error, BatchProcessorInterface $batch_processor, array $batch ) : void {
		$batch_detail_string = '';
		// Log only first and last, as the entire batch may be too big.
		if ( count( $batch ) > 0 ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Logging is for debugging.
			$batch_detail_string = '\n' . print_r(
				array(
					'batch_start' => $batch[0],
					'batch_end'   => end( $batch ),
				),
				true
			);
		}
		$error_message = "Error processing batch for {$batch_processor->get_name()}: {$error->getMessage()}" . $batch_detail_string;

		/**
		 * Filters the error message for a batch processing.
		 *
		 * @param string $error_message The error message that will be logged.
		 * @param \Exception $error The exception that was thrown by the processor.
		 * @param BatchProcessorInterface $batch_processor The processor that threw the exception.
		 * @param array $batch The batch that was being processed.
		 * @return string The actual error message that will be logged.
		 *
		 * @since 6.8.0
		 */
		$error_message = apply_filters( 'wc_batch_processing_log_message', $error_message, $error, $batch_processor, $batch );

		$this->logger->error( $error_message, array( 'exception' => $error ) );
	}
}
Internal/AssignDefaultCategory.php000064400000003631152333011040013253 0ustar00<?php
/**
 * AssignDefaultCategory class file.
 */

namespace Automattic\WooCommerce\Internal;

defined( 'ABSPATH' ) || exit;

/**
 * Class to assign default category to products.
 */
class AssignDefaultCategory {
	/**
	 * Class initialization, to be executed when the class is resolved by the container.
	 *
	 * @internal
	 */
	final public function init() {
		add_action( 'wc_schedule_update_product_default_cat', array( $this, 'maybe_assign_default_product_cat' ) );
	}

	/**
	 * When a product category is deleted, we need to check
	 * if the product has no categories assigned. Then assign
	 * it a default category. We delay this with a scheduled
	 * action job to not block the response.
	 *
	 * @return void
	 */
	public function schedule_action() {
		WC()->queue()->schedule_single(
			time(),
			'wc_schedule_update_product_default_cat',
			array(),
			'wc_update_product_default_cat'
		);
	}

	/**
	 * Assigns default product category for products
	 * that have no categories.
	 *
	 * @return void
	 */
	public function maybe_assign_default_product_cat() {
		global $wpdb;

		$default_category = get_option( 'default_product_cat', 0 );

		if ( $default_category ) {
			$wpdb->query(
				$wpdb->prepare(
					"INSERT INTO {$wpdb->term_relationships} (object_id, term_taxonomy_id)
					SELECT DISTINCT posts.ID, %s FROM {$wpdb->posts} posts
					LEFT JOIN
						(
							SELECT object_id FROM {$wpdb->term_relationships} term_relationships
							LEFT JOIN {$wpdb->term_taxonomy} term_taxonomy ON term_relationships.term_taxonomy_id = term_taxonomy.term_taxonomy_id
							WHERE term_taxonomy.taxonomy = 'product_cat'
						) AS tax_query
					ON posts.ID = tax_query.object_id
					WHERE posts.post_type = 'product'
					AND tax_query.object_id IS NULL",
					$default_category
				)
			);
			wp_cache_flush();
			delete_transient( 'wc_term_counts' );
			wp_update_term_count_now( array( $default_category ), 'product_cat' );
		}
	}
}
Internal/Orders/IppFunctions.php000064400000004243152333011040012703 0ustar00<?php


namespace Automattic\WooCommerce\Internal\Orders;

use WC_Order;

/**
 * Class with methods for handling order In-Person Payments.
 */
class IppFunctions {

	/**
	 * Returns if order is eligible to accept In-Person Payments.
	 *
	 * @param WC_Order $order order that the conditions are checked for.
	 *
	 * @return bool true if order is eligible, false otherwise
	 */
	public static function is_order_in_person_payment_eligible( WC_Order $order ): bool {
		$has_status            = in_array( $order->get_status(), array( 'pending', 'on-hold', 'processing' ), true );
		$has_payment_method    = in_array( $order->get_payment_method(), array( 'cod', 'woocommerce_payments', 'none' ), true );
		$order_is_not_paid     = null === $order->get_date_paid();
		$order_is_not_refunded = empty( $order->get_refunds() );

		$order_has_no_subscription_products = true;
		foreach ( $order->get_items() as $item ) {
			$product = $item->get_product();

			if ( is_object( $product ) && $product->is_type( 'subscription' ) ) {
				$order_has_no_subscription_products = false;
				break;
			}
		}

		return $has_status && $has_payment_method && $order_is_not_paid && $order_is_not_refunded && $order_has_no_subscription_products;
	}

	/**
	 * Returns if store is eligible to accept In-Person Payments.
	 *
	 * @return bool true if store is eligible, false otherwise
	 */
	public static function is_store_in_person_payment_eligible(): bool {
		$is_store_usa_based    = self::has_store_specified_country_currency( 'US', 'USD' );
		$is_store_canada_based = self::has_store_specified_country_currency( 'CA', 'CAD' );

		return $is_store_usa_based || $is_store_canada_based;
	}

	/**
	 * Checks if the store has specified country location and currency used.
	 *
	 * @param string $country country to compare store's country with.
	 * @param string $currency currency to compare store's currency with.
	 *
	 * @return bool true if specified country and currency match the store's ones. false otherwise
	 */
	public static function has_store_specified_country_currency( string $country, string $currency ): bool {
		return ( WC()->countries->get_base_country() === $country && get_woocommerce_currency() === $currency );
	}
}
Internal/Orders/TaxesController.php000064400000003462152333011040013414 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Orders;

/**
 * Class with methods for handling order taxes.
 */
class TaxesController {

	/**
	 * Calculate line taxes via Ajax call.
	 */
	public function calc_line_taxes_via_ajax(): void {
		check_ajax_referer( 'calc-totals', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['items'] ) ) {
			wp_die( -1 );
		}

		$order = $this->calc_line_taxes( $_POST );

		include __DIR__ . '/../../../includes/admin/meta-boxes/views/html-order-items.php';
		wp_die();
	}

	/**
	 * Calculate line taxes programmatically.
	 *
	 * @param array $post_variables Contents of the $_POST array that would be passed in an Ajax call.
	 * @return object The retrieved order object.
	 */
	public function calc_line_taxes( array $post_variables ): object {
		$order_id           = absint( $post_variables['order_id'] );
		$calculate_tax_args = array(
			'country'  => isset( $post_variables['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['country'] ) ) ) : '',
			'state'    => isset( $post_variables['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['state'] ) ) ) : '',
			'postcode' => isset( $post_variables['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['postcode'] ) ) ) : '',
			'city'     => isset( $post_variables['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['city'] ) ) ) : '',
		);

		// Parse the jQuery serialized items.
		$items = array();
		parse_str( wp_unslash( $post_variables['items'] ), $items );

		// Save order items first.
		wc_save_order_items( $order_id, $items );

		// Grab the order and recalculate taxes.
		$order = wc_get_order( $order_id );
		$order->calculate_taxes( $calculate_tax_args );
		$order->calculate_totals( false );

		return $order;
	}
}
Internal/Orders/CouponsController.php000064400000005751152333011040013761 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Orders;

use Exception;

/**
 * Class with methods for handling order coupons.
 */
class CouponsController {

	/**
	 * Add order discount via Ajax.
	 *
	 * @throws Exception If order or coupon is invalid.
	 */
	public function add_coupon_discount_via_ajax(): void {
		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) ) {
			wp_die( -1 );
		}

		$response = array();

		try {
			$order = $this->add_coupon_discount( $_POST );

			ob_start();
			include __DIR__ . '/../../../includes/admin/meta-boxes/views/html-order-items.php';
			$response['html'] = ob_get_clean();
		} catch ( Exception $e ) {
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
		}

		// wp_send_json_success must be outside the try block not to break phpunit tests.
		wp_send_json_success( $response );
	}

	/**
	 * Add order discount programmatically.
	 *
	 * @param array $post_variables Contents of the $_POST array that would be passed in an Ajax call.
	 * @return object The retrieved order object.
	 * @throws \Exception Invalid order or coupon.
	 */
	public function add_coupon_discount( array $post_variables ): object {
		$order_id           = isset( $post_variables['order_id'] ) ? absint( $post_variables['order_id'] ) : 0;
		$order              = wc_get_order( $order_id );
		$calculate_tax_args = array(
			'country'  => isset( $post_variables['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['country'] ) ) ) : '',
			'state'    => isset( $post_variables['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['state'] ) ) ) : '',
			'postcode' => isset( $post_variables['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['postcode'] ) ) ) : '',
			'city'     => isset( $post_variables['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['city'] ) ) ) : '',
		);

		if ( ! $order ) {
			throw new Exception( __( 'Invalid order', 'woocommerce' ) );
		}

		if ( empty( $post_variables['coupon'] ) ) {
			throw new Exception( __( 'Invalid coupon', 'woocommerce' ) );
		}

		// Add user ID and/or email so validation for coupon limits works.
		$user_id_arg    = isset( $post_variables['user_id'] ) ? absint( $post_variables['user_id'] ) : 0;
		$user_email_arg = isset( $post_variables['user_email'] ) ? sanitize_email( wp_unslash( $post_variables['user_email'] ) ) : '';

		if ( $user_id_arg ) {
			$order->set_customer_id( $user_id_arg );
		}
		if ( $user_email_arg ) {
			$order->set_billing_email( $user_email_arg );
		}

		$order->calculate_taxes( $calculate_tax_args );
		$order->calculate_totals( false );

		$result = $order->apply_coupon( wc_format_coupon_code( wp_unslash( $post_variables['coupon'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		if ( is_wp_error( $result ) ) {
			throw new Exception( html_entity_decode( wp_strip_all_tags( $result->get_error_message() ) ) );
		}

		return $order;
	}
}
Internal/Orders/MobileMessagingHandler.php000064400000013002152333011040014616 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Orders;

use DateTime;
use Exception;
use WC_Order;
use WC_Tracker;

/**
 * Prepares formatted mobile deep link navigation link for order mails.
 */
class MobileMessagingHandler {

	private const OPEN_ORDER_INTERVAL_DAYS = 30;

	/**
	 * Prepares mobile messaging with a deep link.
	 *
	 * @param WC_Order $order order that mobile message is created for.
	 * @param ?int     $blog_id  of blog to make a deep link for (will be null if Jetpack is not enabled).
	 * @param DateTime $now      current DateTime.
	 * @param string   $domain URL of the current site.
	 *
	 * @return ?string
	 */
	public static function prepare_mobile_message(
		WC_Order $order,
		?int $blog_id,
		DateTime $now,
		string $domain
	): ?string {
		try {
			$last_mobile_used = self::get_closer_mobile_usage_date();

			$used_app_in_last_month = null !== $last_mobile_used && $last_mobile_used->diff( $now )->days <= self::OPEN_ORDER_INTERVAL_DAYS;
			$has_jetpack            = null !== $blog_id;

			if ( IppFunctions::is_store_in_person_payment_eligible() && IppFunctions::is_order_in_person_payment_eligible( $order ) ) {
				return self::accept_payment_message( $blog_id, $domain );
			} else {
				if ( $used_app_in_last_month && $has_jetpack ) {
					return self::manage_order_message( $blog_id, $order->get_id(), $domain );
				} else {
					return self::no_app_message( $blog_id, $domain );
				}
			}
		} catch ( Exception $e ) {
			return null;
		}
	}

	/**
	 * Returns the closest date of last usage of any mobile app platform.
	 *
	 * @return ?DateTime
	 */
	private static function get_closer_mobile_usage_date(): ?DateTime {
		$mobile_usage = WC_Tracker::get_woocommerce_mobile_usage();

		if ( ! $mobile_usage ) {
			return null;
		}

		$last_ios_used     = self::get_last_used_or_null( 'ios', $mobile_usage );
		$last_android_used = self::get_last_used_or_null( 'android', $mobile_usage );

		return max( $last_android_used, $last_ios_used );
	}

	/**
	 * Returns last used date of specified mobile app platform.
	 *
	 * @param string $platform     mobile platform to check.
	 * @param array  $mobile_usage mobile apps usage data.
	 *
	 * @return ?DateTime last used date of specified mobile app
	 */
	private static function get_last_used_or_null(
		string $platform, array $mobile_usage
	): ?DateTime {
		try {
			if ( array_key_exists( $platform, $mobile_usage ) ) {
				return new DateTime( $mobile_usage[ $platform ]['last_used'] );
			} else {
				return null;
			}
		} catch ( Exception $e ) {
			return null;
		}
	}

	/**
	 * Prepares message with a deep link to mobile payment.
	 *
	 * @param ?int   $blog_id blog id to deep link to.
	 * @param string $domain URL of the current site.
	 *
	 * @return string formatted message
	 */
	private static function accept_payment_message( ?int $blog_id, $domain ): string {
		$deep_link_url = add_query_arg(
			array_merge(
				array(
					'blog_id' => absint( $blog_id ),
				),
				self::prepare_utm_parameters( 'deeplinks_payments', $blog_id, $domain )
			),
			'https://woocommerce.com/mobile/payments'
		);

		return sprintf(
			/* translators: 1: opening link tag 2: closing link tag. */
			esc_html__(
				'%1$sCollect payments easily%2$s from your customers anywhere with our mobile app.',
				'woocommerce'
			),
			'<a href="' . esc_url( $deep_link_url ) . '">',
			'</a>'
		);
	}

	/**
	 * Prepares message with a deep link to manage order details.
	 *
	 * @param int    $blog_id blog id to deep link to.
	 * @param int    $order_id order id to deep link to.
	 * @param string $domain URL of the current site.
	 *
	 * @return string formatted message
	 */
	private static function manage_order_message( int $blog_id, int $order_id, string $domain ): string {
		$deep_link_url = add_query_arg(
			array_merge(
				array(
					'blog_id'  => absint( $blog_id ),
					'order_id' => absint( $order_id ),
				),
				self::prepare_utm_parameters( 'deeplinks_orders_details', $blog_id, $domain )
			),
			'https://woocommerce.com/mobile/orders/details'
		);

		return sprintf(
			/* translators: 1: opening link tag 2: closing link tag. */
			esc_html__(
				'%1$sManage the order%2$s with the app.',
				'woocommerce'
			),
			'<a href="' . esc_url( $deep_link_url ) . '">',
			'</a>'
		);
	}

	/**
	 * Prepares message with a deep link to learn more about mobile app.
	 *
	 * @param ?int   $blog_id blog id used for tracking.
	 * @param string $domain URL of the current site.
	 *
	 * @return string formatted message
	 */
	private static function no_app_message( ?int $blog_id, string $domain ): string {
		$deep_link_url = add_query_arg(
			array_merge(
				array(
					'blog_id' => absint( $blog_id ),
				),
				self::prepare_utm_parameters( 'deeplinks_promote_app', $blog_id, $domain )
			),
			'https://woocommerce.com/mobile'
		);
		return sprintf(
			/* translators: 1: opening link tag 2: closing link tag. */
			esc_html__(
				'Process your orders on the go. %1$sGet the app%2$s.',
				'woocommerce'
			),
			'<a href="' . esc_url( $deep_link_url ) . '">',
			'</a>'
		);
	}

	/**
	 * Prepares array of parameters used by WooCommerce.com for tracking.
	 *
	 * @param string   $campaign name of the deep link campaign.
	 * @param int|null $blog_id blog id of the current site.
	 * @param string   $domain URL of the current site.
	 *
	 * @return array
	 */
	private static function prepare_utm_parameters(
		string $campaign,
		?int $blog_id,
		string $domain
	): array {
		return array(
			'utm_campaign' => $campaign,
			'utm_medium'   => 'email',
			'utm_source'   => $domain,
			'utm_term'     => absint( $blog_id ),
		);
	}
}
Internal/ProductAttributesLookup/LookupDataStore.php000064400000057670152333011040017001 0ustar00<?php
/**
 * LookupDataStore class file.
 */

namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;

use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\StringUtil;

defined( 'ABSPATH' ) || exit;

/**
 * Data store class for the product attributes lookup table.
 */
class LookupDataStore {

	use AccessiblePrivateMethods;

	/**
	 * Types of updates to perform depending on the current changest
	 */

	public const ACTION_NONE         = 0;
	public const ACTION_INSERT       = 1;
	public const ACTION_UPDATE_STOCK = 2;
	public const ACTION_DELETE       = 3;

	/**
	 * The lookup table name.
	 *
	 * @var string
	 */
	private $lookup_table_name;

	/**
	 * LookupDataStore constructor. Makes the feature hidden by default.
	 */
	public function __construct() {
		global $wpdb;

		$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';

		$this->init_hooks();
	}

	/**
	 * Initialize the hooks used by the class.
	 */
	private function init_hooks() {
		self::add_action( 'woocommerce_run_product_attribute_lookup_update_callback', array( $this, 'run_update_callback' ), 10, 2 );
		self::add_filter( 'woocommerce_get_sections_products', array( $this, 'add_advanced_section_to_product_settings' ), 100, 1 );
		self::add_action( 'woocommerce_rest_insert_product', array( $this, 'on_product_created_or_updated_via_rest_api' ), 100, 2 );
		self::add_filter( 'woocommerce_get_settings_products', array( $this, 'add_product_attributes_lookup_table_settings' ), 100, 2 );
	}

	/**
	 * Check if the lookup table exists in the database.
	 *
	 * @return bool
	 */
	public function check_lookup_table_exists() {
		global $wpdb;

		$query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $this->lookup_table_name ) );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		return $this->lookup_table_name === $wpdb->get_var( $query );
	}

	/**
	 * Get the name of the lookup table.
	 *
	 * @return string
	 */
	public function get_lookup_table_name() {
		return $this->lookup_table_name;
	}

	/**
	 * Insert/update the appropriate lookup table entries for a new or modified product or variation.
	 * This must be invoked after a product or a variation is created (including untrashing and duplication)
	 * or modified.
	 *
	 * @param int|\WC_Product $product Product object or product id.
	 * @param null|array      $changeset Changes as provided by 'get_changes' method in the product object, null if it's being created.
	 */
	public function on_product_changed( $product, $changeset = null ) {
		if ( ! $this->check_lookup_table_exists() ) {
			return;
		}

		if ( ! is_a( $product, \WC_Product::class ) ) {
			$product = WC()->call_function( 'wc_get_product', $product );
		}

		$action = $this->get_update_action( $changeset );
		if ( $action !== self::ACTION_NONE ) {
			$this->maybe_schedule_update( $product->get_id(), $action );
		}
	}

	/**
	 * Schedule an update of the product attributes lookup table for a given product.
	 * If an update for the same action is already scheduled, nothing is done.
	 *
	 * If the 'woocommerce_attribute_lookup_direct_update' option is set to 'yes',
	 * the update is done directly, without scheduling.
	 *
	 * @param int $product_id The product id to schedule the update for.
	 * @param int $action The action to perform, one of the ACTION_ constants.
	 */
	private function maybe_schedule_update( int $product_id, int $action ) {
		if ( get_option( 'woocommerce_attribute_lookup_direct_updates' ) === 'yes' ) {
			$this->run_update_callback( $product_id, $action );
			return;
		}

		$args = array( $product_id, $action );

		$queue             = WC()->get_instance_of( \WC_Queue::class );
		$already_scheduled = $queue->search(
			array(
				'hook'   => 'woocommerce_run_product_attribute_lookup_update_callback',
				'args'   => $args,
				'status' => \ActionScheduler_Store::STATUS_PENDING,
			),
			'ids'
		);

		if ( empty( $already_scheduled ) ) {
			$queue->schedule_single(
				WC()->call_function( 'time' ) + 1,
				'woocommerce_run_product_attribute_lookup_update_callback',
				$args,
				'woocommerce-db-updates'
			);
		}
	}

	/**
	 * Perform an update of the lookup table for a specific product.
	 *
	 * @param int $product_id The product id to perform the update for.
	 * @param int $action The action to perform, one of the ACTION_ constants.
	 */
	private function run_update_callback( int $product_id, int $action ) {
		if ( ! $this->check_lookup_table_exists() ) {
			return;
		}

		$product = WC()->call_function( 'wc_get_product', $product_id );
		if ( ! $product ) {
			$action = self::ACTION_DELETE;
		}

		switch ( $action ) {
			case self::ACTION_INSERT:
				$this->delete_data_for( $product_id );
				$this->create_data_for( $product );
				break;
			case self::ACTION_UPDATE_STOCK:
				$this->update_stock_status_for( $product );
				break;
			case self::ACTION_DELETE:
				$this->delete_data_for( $product_id );
				break;
		}
	}

	/**
	 * Determine the type of action to perform depending on the received changeset.
	 *
	 * @param array|null $changeset The changeset received by on_product_changed.
	 * @return int One of the ACTION_ constants.
	 */
	private function get_update_action( $changeset ) {
		if ( is_null( $changeset ) ) {
			// No changeset at all means that the product is new.
			return self::ACTION_INSERT;
		}

		$keys = array_keys( $changeset );

		// Order matters:
		// - The change with the most precedence is a change in catalog visibility
		// (which will result in all data being regenerated or deleted).
		// - Then a change in attributes (all data will be regenerated).
		// - And finally a change in stock status (existing data will be updated).
		// Thus these conditions must be checked in that same order.

		if ( in_array( 'catalog_visibility', $keys, true ) ) {
			$new_visibility = $changeset['catalog_visibility'];
			if ( $new_visibility === 'visible' || $new_visibility === 'catalog' ) {
				return self::ACTION_INSERT;
			} else {
				return self::ACTION_DELETE;
			}
		}

		if ( in_array( 'attributes', $keys, true ) ) {
			return self::ACTION_INSERT;
		}

		if ( array_intersect( $keys, array( 'stock_quantity', 'stock_status', 'manage_stock' ) ) ) {
			return self::ACTION_UPDATE_STOCK;
		}

		return self::ACTION_NONE;
	}

	/**
	 * Update the stock status of the lookup table entries for a given product.
	 *
	 * @param \WC_Product $product The product to update the entries for.
	 */
	private function update_stock_status_for( \WC_Product $product ) {
		global $wpdb;

		$in_stock = $product->is_in_stock();

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
		$wpdb->query(
			$wpdb->prepare(
				'UPDATE ' . $this->lookup_table_name . ' SET in_stock = %d WHERE product_id = %d',
				$in_stock ? 1 : 0,
				$product->get_id()
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Delete the lookup table contents related to a given product or variation,
	 * if it's a variable product it deletes the information for variations too.
	 * This must be invoked after a product or a variation is trashed or deleted.
	 *
	 * @param int|\WC_Product $product Product object or product id.
	 */
	public function on_product_deleted( $product ) {
		if ( ! $this->check_lookup_table_exists() ) {
			return;
		}

		if ( is_a( $product, \WC_Product::class ) ) {
			$product_id = $product->get_id();
		} else {
			$product_id = $product;
		}

		$this->maybe_schedule_update( $product_id, self::ACTION_DELETE );
	}

	/**
	 * Create the lookup data for a given product, if a variable product is passed
	 * the information is created for all of its variations.
	 * This method is intended to be called from the data regenerator.
	 *
	 * @param int|WC_Product $product Product object or id.
	 * @throws \Exception A variation object is passed.
	 */
	public function create_data_for_product( $product ) {
		if ( ! is_a( $product, \WC_Product::class ) ) {
			$product = WC()->call_function( 'wc_get_product', $product );
		}

		if ( $this->is_variation( $product ) ) {
			throw new \Exception( "LookupDataStore::create_data_for_product can't be called for variations." );
		}

		$this->delete_data_for( $product->get_id() );
		$this->create_data_for( $product );
	}

	/**
	 * Create lookup table data for a given product.
	 *
	 * @param \WC_Product $product The product to create the data for.
	 */
	private function create_data_for( \WC_Product $product ) {
		if ( $this->is_variation( $product ) ) {
			$this->create_data_for_variation( $product );
		} elseif ( $this->is_variable_product( $product ) ) {
			$this->create_data_for_variable_product( $product );
		} else {
			$this->create_data_for_simple_product( $product );
		}
	}

	/**
	 * Delete all the lookup table entries for a given product,
	 * if it's a variable product information for variations is deleted too.
	 *
	 * @param int $product_id Simple product id, or main/parent product id for variable products.
	 */
	private function delete_data_for( int $product_id ) {
		global $wpdb;

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
		$wpdb->query(
			$wpdb->prepare(
				'DELETE FROM ' . $this->lookup_table_name . ' WHERE product_id = %d OR product_or_parent_id = %d',
				$product_id,
				$product_id
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Create lookup table entries for a simple (non variable) product.
	 * Assumes that no entries exist yet.
	 *
	 * @param \WC_Product $product The product to create the entries for.
	 */
	private function create_data_for_simple_product( \WC_Product $product ) {
		$product_attributes_data = $this->get_attribute_taxonomies( $product );
		$has_stock               = $product->is_in_stock();
		$product_id              = $product->get_id();
		foreach ( $product_attributes_data as $taxonomy => $data ) {
			$term_ids = $data['term_ids'];
			foreach ( $term_ids as $term_id ) {
				$this->insert_lookup_table_data( $product_id, $product_id, $taxonomy, $term_id, false, $has_stock );
			}
		}
	}

	/**
	 * Create lookup table entries for a variable product.
	 * Assumes that no entries exist yet.
	 *
	 * @param \WC_Product_Variable $product The product to create the entries for.
	 */
	private function create_data_for_variable_product( \WC_Product_Variable $product ) {
		$product_attributes_data       = $this->get_attribute_taxonomies( $product );
		$variation_attributes_data     = array_filter(
			$product_attributes_data,
			function( $item ) {
				return $item['used_for_variations'];
			}
		);
		$non_variation_attributes_data = array_filter(
			$product_attributes_data,
			function( $item ) {
				return ! $item['used_for_variations'];
			}
		);

		$main_product_has_stock = $product->is_in_stock();
		$main_product_id        = $product->get_id();

		foreach ( $non_variation_attributes_data as $taxonomy => $data ) {
			$term_ids = $data['term_ids'];
			foreach ( $term_ids as $term_id ) {
				$this->insert_lookup_table_data( $main_product_id, $main_product_id, $taxonomy, $term_id, false, $main_product_has_stock );
			}
		}

		$term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) );
		$variations             = $this->get_variations_of( $product );

		foreach ( $variation_attributes_data as $taxonomy => $data ) {
			foreach ( $variations as $variation ) {
				$this->insert_lookup_table_data_for_variation( $variation, $taxonomy, $main_product_id, $data['term_ids'], $term_ids_by_slug_cache );
			}
		}
	}

	/**
	 * Create all the necessary lookup data for a given variation.
	 *
	 * @param \WC_Product_Variation $variation The variation to create entries for.
	 */
	private function create_data_for_variation( \WC_Product_Variation $variation ) {
		$main_product = WC()->call_function( 'wc_get_product', $variation->get_parent_id() );

		$product_attributes_data   = $this->get_attribute_taxonomies( $main_product );
		$variation_attributes_data = array_filter(
			$product_attributes_data,
			function( $item ) {
				return $item['used_for_variations'];
			}
		);

		$term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) );

		foreach ( $variation_attributes_data as $taxonomy => $data ) {
			$this->insert_lookup_table_data_for_variation( $variation, $taxonomy, $main_product->get_id(), $data['term_ids'], $term_ids_by_slug_cache );
		}
	}

	/**
	 * Create lookup table entries for a given variation, corresponding to a given taxonomy and a set of term ids.
	 *
	 * @param \WC_Product_Variation $variation The variation to create entries for.
	 * @param string                $taxonomy The taxonomy to create the entries for.
	 * @param int                   $main_product_id The parent product id.
	 * @param array                 $term_ids The term ids to create entries for.
	 * @param array                 $term_ids_by_slug_cache A dictionary of term ids by term slug, as returned by 'get_term_ids_by_slug_cache'.
	 */
	private function insert_lookup_table_data_for_variation( \WC_Product_Variation $variation, string $taxonomy, int $main_product_id, array $term_ids, array $term_ids_by_slug_cache ) {
		$variation_id                 = $variation->get_id();
		$variation_has_stock          = $variation->is_in_stock();
		$variation_definition_term_id = $this->get_variation_definition_term_id( $variation, $taxonomy, $term_ids_by_slug_cache );
		if ( $variation_definition_term_id ) {
			$this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $variation_definition_term_id, true, $variation_has_stock );
		} else {
			$term_ids_for_taxonomy = $term_ids;
			foreach ( $term_ids_for_taxonomy as $term_id ) {
				$this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $term_id, true, $variation_has_stock );
			}
		}
	}

	/**
	 * Get a cache of term ids by slug for a set of taxonomies, with this format:
	 *
	 * [
	 *   'taxonomy' => [
	 *     'slug_1' => id_1,
	 *     'slug_2' => id_2,
	 *     ...
	 *   ], ...
	 * ]
	 *
	 * @param array $taxonomies List of taxonomies to build the cache for.
	 * @return array A dictionary of taxonomies => dictionary of term slug => term id.
	 */
	private function get_term_ids_by_slug_cache( $taxonomies ) {
		$result = array();
		foreach ( $taxonomies as $taxonomy ) {
			$terms               = WC()->call_function(
				'get_terms',
				array(
					'taxonomy'   => $taxonomy,
					'hide_empty' => false,
					'fields'     => 'id=>slug',
				)
			);
			$result[ $taxonomy ] = array_flip( $terms );
		}
		return $result;
	}

	/**
	 * Get the id of the term that defines a variation for a given taxonomy,
	 * or null if there's no such defining id (for variations having "Any <taxonomy>" as the definition)
	 *
	 * @param \WC_Product_Variation $variation The variation to get the defining term id for.
	 * @param string                $taxonomy The taxonomy to get the defining term id for.
	 * @param array                 $term_ids_by_slug_cache A term ids by slug as generated by get_term_ids_by_slug_cache.
	 * @return int|null The term id, or null if there's no defining id for that taxonomy in that variation.
	 */
	private function get_variation_definition_term_id( \WC_Product_Variation $variation, string $taxonomy, array $term_ids_by_slug_cache ) {
		$variation_attributes = $variation->get_attributes();
		$term_slug            = ArrayUtil::get_value_or_default( $variation_attributes, $taxonomy );
		if ( $term_slug ) {
			return $term_ids_by_slug_cache[ $taxonomy ][ $term_slug ];
		} else {
			return null;
		}
	}

	/**
	 * Get the variations of a given variable product.
	 *
	 * @param \WC_Product_Variable $product The product to get the variations for.
	 * @return array An array of WC_Product_Variation objects.
	 */
	private function get_variations_of( \WC_Product_Variable $product ) {
		$variation_ids = $product->get_children();
		return array_map(
			function( $id ) {
				return WC()->call_function( 'wc_get_product', $id );
			},
			$variation_ids
		);
	}

	/**
	 * Check if a given product is a variable product.
	 *
	 * @param \WC_Product $product The product to check.
	 * @return bool True if it's a variable product, false otherwise.
	 */
	private function is_variable_product( \WC_Product $product ) {
		return is_a( $product, \WC_Product_Variable::class );
	}

	/**
	 * Check if a given product is a variation.
	 *
	 * @param \WC_Product $product The product to check.
	 * @return bool True if it's a variation, false otherwise.
	 */
	private function is_variation( \WC_Product $product ) {
		return is_a( $product, \WC_Product_Variation::class );
	}

	/**
	 * Return the list of taxonomies used for variations on a product together with
	 * the associated term ids, with the following format:
	 *
	 * [
	 *   'taxonomy_name' =>
	 *   [
	 *     'term_ids' => [id, id, ...],
	 *     'used_for_variations' => true|false
	 *   ], ...
	 * ]
	 *
	 * @param \WC_Product $product The product to get the attribute taxonomies for.
	 * @return array Information about the attribute taxonomies of the product.
	 */
	private function get_attribute_taxonomies( \WC_Product $product ) {
		$product_attributes = $product->get_attributes();
		$result             = array();
		foreach ( $product_attributes as $taxonomy_name => $attribute_data ) {
			if ( ! $attribute_data->get_id() ) {
				// Custom product attribute, not suitable for attribute-based filtering.
				continue;
			}

			$result[ $taxonomy_name ] = array(
				'term_ids'            => $attribute_data->get_options(),
				'used_for_variations' => $attribute_data->get_variation(),
			);
		}

		return $result;
	}

	/**
	 * Insert one entry in the lookup table.
	 *
	 * @param int    $product_id The product id.
	 * @param int    $product_or_parent_id The product id for non-variable products, the main/parent product id for variations.
	 * @param string $taxonomy Taxonomy name.
	 * @param int    $term_id Term id.
	 * @param bool   $is_variation_attribute True if the taxonomy corresponds to an attribute used to define variations.
	 * @param bool   $has_stock True if the product is in stock.
	 */
	private function insert_lookup_table_data( int $product_id, int $product_or_parent_id, string $taxonomy, int $term_id, bool $is_variation_attribute, bool $has_stock ) {
		global $wpdb;

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
		$wpdb->query(
			$wpdb->prepare(
				'INSERT INTO ' . $this->lookup_table_name . ' (
					  product_id,
					  product_or_parent_id,
					  taxonomy,
					  term_id,
					  is_variation_attribute,
					  in_stock)
					VALUES
					  ( %d, %d, %s, %d, %d, %d )',
				$product_id,
				$product_or_parent_id,
				$taxonomy,
				$term_id,
				$is_variation_attribute ? 1 : 0,
				$has_stock ? 1 : 0
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Handler for the woocommerce_rest_insert_product hook.
	 * Needed to update the lookup table when the REST API batch insert/update endpoints are used.
	 *
	 * @param \WP_Post         $product The post representing the created or updated product.
	 * @param \WP_REST_Request $request The REST request that caused the hook to be fired.
	 * @return void
	 */
	private function on_product_created_or_updated_via_rest_api( \WP_Post $product, \WP_REST_Request $request ): void {
		if ( StringUtil::ends_with( $request->get_route(), '/batch' ) ) {
			$this->on_product_changed( $product->ID );
		}
	}

	/**
	 * Tells if a lookup table regeneration is currently in progress.
	 *
	 * @return bool True if a lookup table regeneration is already in progress.
	 */
	public function regeneration_is_in_progress() {
		return get_option( 'woocommerce_attribute_lookup_regeneration_in_progress', null ) === 'yes';
	}

	/**
	 * Set a permanent flag (via option) indicating that the lookup table regeneration is in process.
	 */
	public function set_regeneration_in_progress_flag() {
		update_option( 'woocommerce_attribute_lookup_regeneration_in_progress', 'yes' );
	}

	/**
	 * Remove the flag indicating that the lookup table regeneration is in process.
	 */
	public function unset_regeneration_in_progress_flag() {
		delete_option( 'woocommerce_attribute_lookup_regeneration_in_progress' );
	}

	/**
	 * Set a flag indicating that the last lookup table regeneration process started was aborted.
	 */
	public function set_regeneration_aborted_flag() {
		update_option( 'woocommerce_attribute_lookup_regeneration_aborted', 'yes' );
	}

	/**
	 * Remove the flag indicating that the last lookup table regeneration process started was aborted.
	 */
	public function unset_regeneration_aborted_flag() {
		delete_option( 'woocommerce_attribute_lookup_regeneration_aborted' );
	}

	/**
	 * Tells if the last lookup table regeneration process started was aborted
	 * (via deleting the 'woocommerce_attribute_lookup_regeneration_in_progress' option).
	 *
	 * @return bool True if the last lookup table regeneration process was aborted.
	 */
	public function regeneration_was_aborted(): bool {
		return get_option( 'woocommerce_attribute_lookup_regeneration_aborted' ) === 'yes';
	}

	/**
	 * Check if the lookup table contains any entry at all.
	 *
	 * @return bool True if the table contains entries, false if the table is empty.
	 */
	public function lookup_table_has_data(): bool {
		global $wpdb;

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return ( (int) $wpdb->get_var( "SELECT EXISTS (SELECT 1 FROM {$this->lookup_table_name})" ) ) !== 0;
	}

	/**
	 * Handler for 'woocommerce_get_sections_products', adds the "Advanced" section to the product settings.
	 *
	 * @param array $products Original array of settings sections.
	 * @return array New array of settings sections.
	 */
	private function add_advanced_section_to_product_settings( array $products ): array {
		if ( $this->check_lookup_table_exists() ) {
			$products['advanced'] = __( 'Advanced', 'woocommerce' );
		}

		return $products;
	}

	/**
	 * Handler for 'woocommerce_get_settings_products', adds the settings related to the product attributes lookup table.
	 *
	 * @param array  $settings Original settings configuration array.
	 * @param string $section_id Settings section identifier.
	 * @return array New settings configuration array.
	 */
	private function add_product_attributes_lookup_table_settings( array $settings, string $section_id ): array {
		if ( $section_id === 'advanced' && $this->check_lookup_table_exists() ) {
			$title_item = array(
				'title' => __( 'Product attributes lookup table', 'woocommerce' ),
				'type'  => 'title',
			);

			$regeneration_is_in_progress = $this->regeneration_is_in_progress();

			if ( $regeneration_is_in_progress ) {
				$title_item['desc'] = __( 'These settings are not available while the lookup table regeneration is in progress.', 'woocommerce' );
			}

			$settings[] = $title_item;

			if ( ! $regeneration_is_in_progress ) {
				$regeneration_aborted_warning =
					$this->regeneration_was_aborted() ?
						sprintf(
							"<p><strong style='color: #E00000'>%s</strong></p><p>%s</p>",
							__( 'WARNING: The product attributes lookup table regeneration process was aborted.', 'woocommerce' ),
							__( 'This means that the table is probably in an inconsistent state. It\'s recommended to run a new regeneration process or to resume the aborted process (Status - Tools - Regenerate the product attributes lookup table/Resume the product attributes lookup table regeneration) before enabling the table usage.', 'woocommerce' )
						) : null;

				$settings[] = array(
					'title'         => __( 'Enable table usage', 'woocommerce' ),
					'desc'          => __( 'Use the product attributes lookup table for catalog filtering.', 'woocommerce' ),
					'desc_tip'      => $regeneration_aborted_warning,
					'id'            => 'woocommerce_attribute_lookup_enabled',
					'default'       => 'no',
					'type'          => 'checkbox',
					'checkboxgroup' => 'start',
				);

				$settings[] = array(
					'title'         => __( 'Direct updates', 'woocommerce' ),
					'desc'          => __( 'Update the table directly upon product changes, instead of scheduling a deferred update.', 'woocommerce' ),
					'id'            => 'woocommerce_attribute_lookup_direct_updates',
					'default'       => 'no',
					'type'          => 'checkbox',
					'checkboxgroup' => 'start',
				);
			}

			$settings[] = array( 'type' => 'sectionend' );
		}

		return $settings;
	}
}
Internal/ProductAttributesLookup/DataRegenerator.php000064400000046437152333011040016767 0ustar00<?php
/**
 * DataRegenerator class file.
 */

namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;

use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;

defined( 'ABSPATH' ) || exit;

/**
 * This class handles the (re)generation of the product attributes lookup table.
 * It schedules the regeneration in small product batches by itself, so it can be used outside the
 * regular WooCommerce data regenerations mechanism.
 *
 * After the regeneration is completed a wp_wc_product_attributes_lookup table will exist with entries for
 * all the products that existed when initiate_regeneration was invoked; entries for products created after that
 * are supposed to be created/updated by the appropriate data store classes (or by the code that uses
 * the data store classes) whenever a product is created/updated.
 *
 * Additionally, after the regeneration is completed a 'woocommerce_attribute_lookup_enabled' option
 * with a value of 'yes' will have been created, thus effectively enabling the table usage
 * (with an exception: if the regeneration was manually aborted via deleting the
 * 'woocommerce_attribute_lookup_regeneration_in_progress' option) the option will be set to 'no'.
 *
 * This class also adds two entries to the Status - Tools menu: one for manually regenerating the table contents,
 * and another one for enabling or disabling the actual lookup table usage.
 */
class DataRegenerator {

	use AccessiblePrivateMethods;

	public const PRODUCTS_PER_GENERATION_STEP = 10;

	/**
	 * The data store to use.
	 *
	 * @var LookupDataStore
	 */
	private $data_store;

	/**
	 * The lookup table name.
	 *
	 * @var string
	 */
	private $lookup_table_name;

	/**
	 * DataRegenerator constructor.
	 */
	public function __construct() {
		global $wpdb;

		$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';

		self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 1, 999 );
		self::add_action( 'woocommerce_run_product_attribute_lookup_regeneration_callback', array( $this, 'run_regeneration_step_callback' ) );
		self::add_action( 'woocommerce_installed', array( $this, 'run_woocommerce_installed_callback' ) );
	}

	/**
	 * Class initialization, invoked by the DI container.
	 *
	 * @internal
	 * @param LookupDataStore $data_store The data store to use.
	 */
	final public function init( LookupDataStore $data_store ) {
		$this->data_store = $data_store;
	}

	/**
	 * Initialize the regeneration procedure:
	 * deletes the lookup table and related options if they exist,
	 * then it creates the table and runs the first step of the regeneration process.
	 *
	 * This method is intended ONLY to be used as a callback for a db update in wc-update-functions,
	 * regeneration triggered from the tools page will use initiate_regeneration_from_tools_page instead.
	 */
	public function initiate_regeneration() {
		$this->data_store->unset_regeneration_aborted_flag();
		$this->enable_or_disable_lookup_table_usage( false );

		$this->delete_all_attributes_lookup_data();
		$products_exist = $this->initialize_table_and_data();
		if ( $products_exist ) {
			$this->enqueue_regeneration_step_run();
		} else {
			$this->finalize_regeneration( true );
		}
	}

	/**
	 * Delete all the existing data related to the lookup table, including the table itself.
	 */
	private function delete_all_attributes_lookup_data() {
		global $wpdb;

		delete_option( 'woocommerce_attribute_lookup_enabled' );
		delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
		delete_option( 'woocommerce_attribute_lookup_processed_count' );
		$this->data_store->unset_regeneration_in_progress_flag();

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$wpdb->query( 'DROP TABLE IF EXISTS ' . $this->lookup_table_name );
	}

	/**
	 * Create the lookup table and initialize the options that will be temporarily used
	 * while the regeneration is in progress.
	 *
	 * @return bool True if there's any product at all in the database, false otherwise.
	 */
	private function initialize_table_and_data() {
		global $wpdb;

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$wpdb->query( $this->get_table_creation_sql() );

		$last_existing_product_id = $this->get_last_existing_product_id();
		if ( ! $last_existing_product_id ) {
			// No products exist, nothing to (re)generate.
			return false;
		}

		$this->data_store->set_regeneration_in_progress_flag();
		update_option( 'woocommerce_attribute_lookup_last_product_id_to_process', $last_existing_product_id );
		update_option( 'woocommerce_attribute_lookup_processed_count', 0 );

		return true;
	}

	/**
	 * Get the highest existing product id.
	 *
	 * @return int|null Highest existing product id, or null if no products exist at all.
	 */
	private function get_last_existing_product_id(): ?int {
		$last_existing_product_id_array =
			WC()->call_function(
				'wc_get_products',
				array(
					'return'  => 'ids',
					'limit'   => 1,
					'orderby' => array(
						'ID' => 'DESC',
					),
				)
			);

		return empty( $last_existing_product_id_array ) ? null : current( $last_existing_product_id_array );
	}

	/**
	 * Action scheduler callback, performs one regeneration step and then
	 * schedules the next step if necessary.
	 */
	private function run_regeneration_step_callback() {
		if ( ! $this->data_store->regeneration_is_in_progress() ) {
			// No regeneration in progress at this point means that the regeneration process
			// was manually aborted via deleting the 'woocommerce_attribute_lookup_regeneration_in_progress' option.
			$this->data_store->set_regeneration_aborted_flag();
			$this->finalize_regeneration( false );
			return;
		}

		$result = $this->do_regeneration_step();
		if ( $result ) {
			$this->enqueue_regeneration_step_run();
		} else {
			$this->finalize_regeneration( true );
		}
	}

	/**
	 * Enqueue one regeneration step in action scheduler.
	 */
	private function enqueue_regeneration_step_run() {
		$queue = WC()->get_instance_of( \WC_Queue::class );
		$queue->schedule_single(
			WC()->call_function( 'time' ) + 1,
			'woocommerce_run_product_attribute_lookup_regeneration_callback',
			array(),
			'woocommerce-db-updates'
		);
	}

	/**
	 * Perform one regeneration step: grabs a chunk of products and creates
	 * the appropriate entries for them in the lookup table.
	 *
	 * @return bool True if more steps need to be run, false otherwise.
	 */
	private function do_regeneration_step() {
		/**
		 * Filter to alter the count of products that will be processed in each step of the product attributes lookup table regeneration process.
		 *
		 * @since 6.3
		 * @param int $count Default processing step size.
		 */
		$products_per_generation_step = apply_filters( 'woocommerce_attribute_lookup_regeneration_step_size', self::PRODUCTS_PER_GENERATION_STEP );

		$products_already_processed = get_option( 'woocommerce_attribute_lookup_processed_count', 0 );

		$product_ids = WC()->call_function(
			'wc_get_products',
			array(
				'limit'   => $products_per_generation_step,
				'offset'  => $products_already_processed,
				'orderby' => array(
					'ID' => 'ASC',
				),
				'return'  => 'ids',
			)
		);

		if ( ! $product_ids ) {
			return false;
		}

		foreach ( $product_ids as $id ) {
			$this->data_store->create_data_for_product( $id );
		}

		$products_already_processed += count( $product_ids );
		update_option( 'woocommerce_attribute_lookup_processed_count', $products_already_processed );

		$last_product_id_to_process = get_option( 'woocommerce_attribute_lookup_last_product_id_to_process', PHP_INT_MAX );
		return end( $product_ids ) < $last_product_id_to_process;
	}

	/**
	 * Cleanup/final option setup after the regeneration has been completed.
	 *
	 * @param bool $enable_usage Whether the table usage should be enabled or not.
	 */
	private function finalize_regeneration( bool $enable_usage ) {
		delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
		delete_option( 'woocommerce_attribute_lookup_processed_count' );
		update_option( 'woocommerce_attribute_lookup_enabled', $enable_usage ? 'yes' : 'no' );
		$this->data_store->unset_regeneration_in_progress_flag();
	}

	/**
	 * Add a 'Regenerate product attributes lookup table' entry to the Status - Tools page.
	 *
	 * @param array $tools_array The tool definitions array that is passed ro the woocommerce_debug_tools filter.
	 * @return array The tools array with the entry added.
	 */
	private function add_initiate_regeneration_entry_to_tools_array( array $tools_array ) {
		if ( ! $this->data_store->check_lookup_table_exists() ) {
			return $tools_array;
		}

		$generation_is_in_progress = $this->data_store->regeneration_is_in_progress();
		$generation_was_aborted    = $this->data_store->regeneration_was_aborted();

		$entry = array(
			'name'             => __( 'Regenerate the product attributes lookup table', 'woocommerce' ),
			'desc'             => __( 'This tool will regenerate the product attributes lookup table data from existing product(s) data. This process may take a while.', 'woocommerce' ),
			'requires_refresh' => true,
			'callback'         => function() {
				$this->initiate_regeneration_from_tools_page();
				return __( 'Product attributes lookup table data is regenerating', 'woocommerce' );

			},
			'selector'         => array(
				'description'   => __( 'Select a product to regenerate the data for, or leave empty for a full table regeneration:', 'woocommerce' ),
				'class'         => 'wc-product-search',
				'search_action' => 'woocommerce_json_search_products',
				'name'          => 'regenerate_product_attribute_lookup_data_product_id',
				'placeholder'   => esc_attr__( 'Search for a product&hellip;', 'woocommerce' ),
			),
		);

		if ( $generation_is_in_progress ) {
			$entry['button'] = sprintf(
				/* translators: %d: How many products have been processed so far. */
				__( 'Filling in progress (%d)', 'woocommerce' ),
				get_option( 'woocommerce_attribute_lookup_processed_count', 0 )
			);
			$entry['disabled'] = true;
		} else {
			$entry['button'] = __( 'Regenerate', 'woocommerce' );
		}

		$tools_array['regenerate_product_attributes_lookup_table'] = $entry;

		if ( $generation_is_in_progress ) {
			$entry = array(
				'name'             => __( 'Abort the product attributes lookup table regeneration', 'woocommerce' ),
				'desc'             => __( 'This tool will abort the regenerate product attributes lookup table regeneration. After this is done the process can be either started over, or resumed to continue where it stopped.', 'woocommerce' ),
				'requires_refresh' => true,
				'callback'         => function() {
					$this->abort_regeneration_from_tools_page();
					return __( 'Product attributes lookup table regeneration process has been aborted.', 'woocommerce' );
				},
				'button'           => __( 'Abort', 'woocommerce' ),
			);
			$tools_array['abort_product_attributes_lookup_table_regeneration'] = $entry;
		} elseif ( $generation_was_aborted ) {
			$processed_count = get_option( 'woocommerce_attribute_lookup_processed_count', 0 );
			$entry           = array(
				'name'             => __( 'Resume the product attributes lookup table regeneration', 'woocommerce' ),
				'desc'             =>
					sprintf(
						/* translators: %1$s = count of products already processed. */
						__( 'This tool will resume the product attributes lookup table regeneration at the point in which it was aborted (%1$s products were already processed).', 'woocommerce' ),
						$processed_count
					),
				'requires_refresh' => true,
				'callback'         => function() {
					$this->resume_regeneration_from_tools_page();
					return __( 'Product attributes lookup table regeneration process has been resumed.', 'woocommerce' );
				},
				'button'           => __( 'Resume', 'woocommerce' ),
			);
			$tools_array['resume_product_attributes_lookup_table_regeneration'] = $entry;
		}

		return $tools_array;
	}

	/**
	 * Callback to initiate the regeneration process from the Status - Tools page.
	 *
	 * @throws \Exception The regeneration is already in progress.
	 */
	private function initiate_regeneration_from_tools_page() {
		$this->verify_tool_execution_nonce();

		//phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( isset( $_REQUEST['regenerate_product_attribute_lookup_data_product_id'] ) ) {
			$product_id = (int) $_REQUEST['regenerate_product_attribute_lookup_data_product_id'];
			$this->check_can_do_lookup_table_regeneration( $product_id );
			$this->data_store->create_data_for_product( $product_id );
		} else {
			$this->check_can_do_lookup_table_regeneration();
			$this->initiate_regeneration();
		}
		//phpcs:enable WordPress.Security.NonceVerification.Recommended
	}

	/**
	 * Enable or disable the actual lookup table usage.
	 *
	 * @param bool $enable True to enable, false to disable.
	 * @throws \Exception A lookup table regeneration is currently in progress.
	 */
	private function enable_or_disable_lookup_table_usage( $enable ) {
		if ( $this->data_store->regeneration_is_in_progress() ) {
			throw new \Exception( "Can't enable or disable the attributes lookup table usage while it's regenerating." );
		}

		update_option( 'woocommerce_attribute_lookup_enabled', $enable ? 'yes' : 'no' );
	}

	/**
	 * Check if everything is good to go to perform a complete or per product lookup table data regeneration
	 * and throw an exception if not.
	 *
	 * @param mixed $product_id The product id to check the regeneration viability for, or null to check if a complete regeneration is possible.
	 * @throws \Exception Something prevents the regeneration from starting.
	 */
	private function check_can_do_lookup_table_regeneration( $product_id = null ) {
		if ( $product_id && ! $this->data_store->check_lookup_table_exists() ) {
			throw new \Exception( "Can't do product attribute lookup data regeneration: lookup table doesn't exist" );
		}
		if ( $this->data_store->regeneration_is_in_progress() ) {
			throw new \Exception( "Can't do product attribute lookup data regeneration: regeneration is already in progress" );
		}
		if ( $product_id && ! wc_get_product( $product_id ) ) {
			throw new \Exception( "Can't do product attribute lookup data regeneration: product doesn't exist" );
		}
	}

	/**
	 * Callback to abort the regeneration process from the Status - Tools page.
	 *
	 * @throws \Exception The lookup table doesn't exist, or there's no regeneration process in progress to abort.
	 */
	private function abort_regeneration_from_tools_page() {
		$this->verify_tool_execution_nonce();

		if ( ! $this->data_store->check_lookup_table_exists() ) {
			throw new \Exception( "Can't abort the product attribute lookup data regeneration process: lookup table doesn't exist" );
		}
		if ( ! $this->data_store->regeneration_is_in_progress() ) {
			throw new \Exception( "Can't abort the product attribute lookup data regeneration process since it's not currently in progress" );
		}

		$queue = WC()->get_instance_of( \WC_Queue::class );
		$queue->cancel_all( 'woocommerce_run_product_attribute_lookup_regeneration_callback' );
		$this->data_store->unset_regeneration_in_progress_flag();
		$this->data_store->set_regeneration_aborted_flag();
		$this->enable_or_disable_lookup_table_usage( false );

		// Note that we are NOT deleting the options that track the regeneration progress (processed count, last product id to process).
		// This is on purpose so that the regeneration can be resumed where it stopped.
	}

	/**
	 * Callback to resume the regeneration process from the Status - Tools page.
	 *
	 * @throws \Exception The lookup table doesn't exist, or a regeneration process is already in place.
	 */
	private function resume_regeneration_from_tools_page() {
		$this->verify_tool_execution_nonce();

		if ( ! $this->data_store->check_lookup_table_exists() ) {
			throw new \Exception( "Can't resume the product attribute lookup data regeneration process: lookup table doesn't exist" );
		}
		if ( $this->data_store->regeneration_is_in_progress() ) {
			throw new \Exception( "Can't resume the product attribute lookup data regeneration process: regeneration is already in progress" );
		}

		$this->data_store->unset_regeneration_aborted_flag();
		$this->data_store->set_regeneration_in_progress_flag();
		$this->enqueue_regeneration_step_run();
	}

	/**
	 * Verify the validity of the nonce received when executing a tool from the Status - Tools page.
	 *
	 * @throws \Exception Missing or invalid nonce received.
	 */
	private function verify_tool_execution_nonce() {
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
		if ( ! isset( $_REQUEST['_wpnonce'] ) || wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) === false ) {
			throw new \Exception( 'Invalid nonce' );
		}
	}

	/**
	 * Get the name of the product attributes lookup table.
	 *
	 * @return string
	 */
	public function get_lookup_table_name() {
		return $this->lookup_table_name;
	}

	/**
	 * Get the SQL statement that creates the product attributes lookup table, including the indices.
	 *
	 * @return string
	 */
	public function get_table_creation_sql() {
		global $wpdb;

		$collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : '';

		return "CREATE TABLE {$this->lookup_table_name} (
 product_id bigint(20) NOT NULL,
 product_or_parent_id bigint(20) NOT NULL,
 taxonomy varchar(32) NOT NULL,
 term_id bigint(20) NOT NULL,
 is_variation_attribute tinyint(1) NOT NULL,
 in_stock tinyint(1) NOT NULL,
 INDEX is_variation_attribute_term_id (is_variation_attribute, term_id),
 PRIMARY KEY  ( `product_or_parent_id`, `term_id`, `product_id`, `taxonomy` )
) $collate;";
	}

	/**
	 * Create the primary key for the table if it doesn't exist already.
	 * It also deletes the product_or_parent_id_term_id index if it exists, since it's now redundant.
	 *
	 * @return void
	 */
	public function create_table_primary_index() {
		$database_util = wc_get_container()->get( DatabaseUtil::class );
		$database_util->create_primary_key( $this->lookup_table_name, array( 'product_or_parent_id', 'term_id', 'product_id', 'taxonomy' ) );
		$database_util->drop_table_index( $this->lookup_table_name, 'product_or_parent_id_term_id' );

		if ( empty( $database_util->get_index_columns( $this->lookup_table_name ) ) ) {
			wc_get_logger()->error( "The creation of the primary key for the {$this->lookup_table_name} table failed" );
		}

		if ( ! empty( $database_util->get_index_columns( $this->lookup_table_name, 'product_or_parent_id_term_id' ) ) ) {
			wc_get_logger()->error( "Dropping the product_or_parent_id_term_id index from the {$this->lookup_table_name} table failed" );
		}
	}

	/**
	 * Run additional setup needed after a WooCommerce install or update finishes.
	 */
	private function run_woocommerce_installed_callback() {
		// The table must exist at this point (created via dbDelta), but we check just in case.
		if ( ! $this->data_store->check_lookup_table_exists() ) {
			return;
		}

		// If a table regeneration is in progress, leave it alone.
		if ( $this->data_store->regeneration_is_in_progress() ) {
			return;
		}

		// If the lookup table has data, or if it's empty because there are no products yet, we're good.
		// Otherwise (lookup table is empty but products exist) we need to initiate a regeneration if one isn't already in progress.
		if ( $this->data_store->lookup_table_has_data() || ! $this->get_last_existing_product_id() ) {
			$must_enable = get_option( 'woocommerce_attribute_lookup_enabled' ) !== 'no';
			$this->finalize_regeneration( $must_enable );
		} else {
			$this->initiate_regeneration();
		}
	}
}
Internal/ProductAttributesLookup/Filterer.php000064400000027715152333011040015472 0ustar00<?php
/**
 * Filterer class file.
 */

namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;

defined( 'ABSPATH' ) || exit;


/**
 * Helper class for filtering products using the product attributes lookup table.
 */
class Filterer {

	/**
	 * The product attributes lookup data store to use.
	 *
	 * @var LookupDataStore
	 */
	private $data_store;

	/**
	 * The name of the product attributes lookup table.
	 *
	 * @var string
	 */
	private $lookup_table_name;

	/**
	 * Class initialization, invoked by the DI container.
	 *
	 * @internal
	 * @param LookupDataStore $data_store The data store to use.
	 */
	final public function init( LookupDataStore $data_store ) {
		$this->data_store        = $data_store;
		$this->lookup_table_name = $data_store->get_lookup_table_name();
	}

	/**
	 * Checks if the product attribute filtering via lookup table feature is enabled.
	 *
	 * @return bool
	 */
	public function filtering_via_lookup_table_is_active() {
		return 'yes' === get_option( 'woocommerce_attribute_lookup_enabled' );
	}

	/**
	 * Adds post clauses for filtering via lookup table.
	 * This method should be invoked within a 'posts_clauses' filter.
	 *
	 * @param array     $args Product query clauses as supplied to the 'posts_clauses' filter.
	 * @param \WP_Query $wp_query Current product query as supplied to the 'posts_clauses' filter.
	 * @param array     $attributes_to_filter_by Attribute filtering data as generated by WC_Query::get_layered_nav_chosen_attributes.
	 * @return array The updated product query clauses.
	 */
	public function filter_by_attribute_post_clauses( array $args, \WP_Query $wp_query, array $attributes_to_filter_by ) {
		global $wpdb;

		if ( ! $wp_query->is_main_query() || ! $this->filtering_via_lookup_table_is_active() ) {
			return $args;
		}

		// The extra derived table ("SELECT product_or_parent_id FROM") is needed for performance
		// (causes the filtering subquery to be executed only once).
		$clause_root = " {$wpdb->posts}.ID IN ( SELECT product_or_parent_id FROM (";
		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
			$in_stock_clause = ' AND in_stock = 1';
		} else {
			$in_stock_clause = '';
		}

		$attribute_ids_for_and_filtering = array();

		foreach ( $attributes_to_filter_by as $taxonomy => $data ) {
			$all_terms                  = get_terms( $taxonomy, array( 'hide_empty' => false ) );
			$term_ids_by_slug           = wp_list_pluck( $all_terms, 'term_id', 'slug' );
			$term_ids_to_filter_by      = array_values( array_intersect_key( $term_ids_by_slug, array_flip( $data['terms'] ) ) );
			$term_ids_to_filter_by      = array_map( 'absint', $term_ids_to_filter_by );
			$term_ids_to_filter_by_list = '(' . join( ',', $term_ids_to_filter_by ) . ')';
			$is_and_query               = 'and' === $data['query_type'];

			$count = count( $term_ids_to_filter_by );

			if ( 0 !== $count ) {
				if ( $is_and_query && $count > 1 ) {
					$attribute_ids_for_and_filtering = array_merge( $attribute_ids_for_and_filtering, $term_ids_to_filter_by );
				} else {
					$clauses[] = "
							{$clause_root}
							SELECT product_or_parent_id
							FROM {$this->lookup_table_name} lt
							WHERE term_id in {$term_ids_to_filter_by_list}
							{$in_stock_clause}
						)";
				}
			}
		}

		if ( ! empty( $attribute_ids_for_and_filtering ) ) {
			$count                      = count( $attribute_ids_for_and_filtering );
			$term_ids_to_filter_by_list = '(' . join( ',', $attribute_ids_for_and_filtering ) . ')';
			$clauses[]                  = "
				{$clause_root}
				SELECT product_or_parent_id
				FROM {$this->lookup_table_name} lt
				WHERE is_variation_attribute=0
				{$in_stock_clause}
				AND term_id in {$term_ids_to_filter_by_list}
				GROUP BY product_id
				HAVING COUNT(product_id)={$count}
				UNION
				SELECT product_or_parent_id
				FROM {$this->lookup_table_name} lt
				WHERE is_variation_attribute=1
				{$in_stock_clause}
				AND term_id in {$term_ids_to_filter_by_list}
			)";
		}

		if ( ! empty( $clauses ) ) {
			// "temp" is needed because the extra derived tables require an alias.
			$args['where'] .= ' AND (' . join( ' temp ) AND ', $clauses ) . ' temp ))';
		} elseif ( ! empty( $attributes_to_filter_by ) ) {
			$args['where'] .= ' AND 1=0';
		}

		return $args;
	}

	/**
	 * Count products within certain terms, taking the main WP query into consideration,
	 * for the WC_Widget_Layered_Nav widget.
	 *
	 * This query allows counts to be generated based on the viewed products, not all products.
	 *
	 * @param  array  $term_ids Term IDs.
	 * @param  string $taxonomy Taxonomy.
	 * @param  string $query_type Query Type.
	 * @return array
	 */
	public function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) {
		global $wpdb;

		$use_lookup_table = $this->filtering_via_lookup_table_is_active();

		$tax_query  = \WC_Query::get_main_tax_query();
		$meta_query = \WC_Query::get_main_meta_query();
		if ( 'or' === $query_type ) {
			foreach ( $tax_query as $key => $query ) {
				if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) {
					unset( $tax_query[ $key ] );
				}
			}
		}

		$meta_query = new \WP_Meta_Query( $meta_query );
		$tax_query  = new \WP_Tax_Query( $tax_query );

		if ( $use_lookup_table ) {
			$query = $this->get_product_counts_query_using_lookup_table( $tax_query, $meta_query, $taxonomy, $term_ids );
		} else {
			$query = $this->get_product_counts_query_not_using_lookup_table( $tax_query, $meta_query, $term_ids );
		}

		$query     = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query );
		$query_sql = implode( ' ', $query );

		// We have a query - let's see if cached results of this query already exist.
		$query_hash = md5( $query_sql );
		// Maybe store a transient of the count values.
		$cache = apply_filters( 'woocommerce_layered_nav_count_maybe_cache', true );
		if ( true === $cache ) {
			$cached_counts = (array) get_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ) );
		} else {
			$cached_counts = array();
		}
		if ( ! isset( $cached_counts[ $query_hash ] ) ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$results                      = $wpdb->get_results( $query_sql, ARRAY_A );
			$counts                       = array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) );
			$cached_counts[ $query_hash ] = $counts;
			if ( true === $cache ) {
				set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, DAY_IN_SECONDS );
			}
		}
		return array_map( 'absint', (array) $cached_counts[ $query_hash ] );
	}

	/**
	 * Get the query for counting products by terms using the product attributes lookup table.
	 *
	 * @param \WP_Tax_Query  $tax_query The current main tax query.
	 * @param \WP_Meta_Query $meta_query The current main meta query.
	 * @param string         $taxonomy The attribute name to get the term counts for.
	 * @param string         $term_ids The term ids to include in the search.
	 * @return array An array of SQL query parts.
	 */
	private function get_product_counts_query_using_lookup_table( $tax_query, $meta_query, $taxonomy, $term_ids ) {
		global $wpdb;

		$meta_query_sql    = $meta_query->get_sql( 'post', $this->lookup_table_name, 'product_or_parent_id' );
		$tax_query_sql     = $tax_query->get_sql( $this->lookup_table_name, 'product_or_parent_id' );
		$hide_out_of_stock = 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' );
		$in_stock_clause   = $hide_out_of_stock ? ' AND in_stock = 1' : '';

		$query['select'] = 'SELECT COUNT(DISTINCT product_or_parent_id) as term_count, term_id as term_count_id';
		$query['from']   = "FROM {$this->lookup_table_name}";
		$query['join']   = "
			{$tax_query_sql['join']} {$meta_query_sql['join']}
			INNER JOIN {$wpdb->posts} ON {$wpdb->posts}.ID = {$this->lookup_table_name}.product_or_parent_id";

		$term_ids_sql   = $this->get_term_ids_sql( $term_ids );
		$query['where'] = "
			WHERE {$wpdb->posts}.post_type IN ( 'product' )
			AND {$wpdb->posts}.post_status = 'publish'
			{$tax_query_sql['where']} {$meta_query_sql['where']}
			AND {$this->lookup_table_name}.taxonomy='{$taxonomy}'
			AND {$this->lookup_table_name}.term_id IN $term_ids_sql
			{$in_stock_clause}";

		if ( ! empty( $term_ids ) ) {
			$attributes_to_filter_by = \WC_Query::get_layered_nav_chosen_attributes();

			if ( ! empty( $attributes_to_filter_by ) ) {
				$and_term_ids = array();

				foreach ( $attributes_to_filter_by as $taxonomy => $data ) {
					if ( 'and' !== $data['query_type'] ) {
						continue;
					}
					$all_terms             = get_terms( $taxonomy, array( 'hide_empty' => false ) );
					$term_ids_by_slug      = wp_list_pluck( $all_terms, 'term_id', 'slug' );
					$term_ids_to_filter_by = array_values( array_intersect_key( $term_ids_by_slug, array_flip( $data['terms'] ) ) );
					$and_term_ids          = array_merge( $and_term_ids, $term_ids_to_filter_by );
				}

				if ( ! empty( $and_term_ids ) ) {
					$terms_count   = count( $and_term_ids );
					$term_ids_list = '(' . join( ',', $and_term_ids ) . ')';
					// The extra derived table ("SELECT product_or_parent_id FROM") is needed for performance
					// (causes the filtering subquery to be executed only once).
					$query['where'] .= "
						AND product_or_parent_id IN ( SELECT product_or_parent_id FROM (
							SELECT product_or_parent_id
							FROM {$this->lookup_table_name} lt
							WHERE is_variation_attribute=0
							{$in_stock_clause}
							AND term_id in {$term_ids_list}
							GROUP BY product_id
							HAVING COUNT(product_id)={$terms_count}
							UNION
							SELECT product_or_parent_id
							FROM {$this->lookup_table_name} lt
							WHERE is_variation_attribute=1
							{$in_stock_clause}
							AND term_id in {$term_ids_list}
						) temp )";
				}
			} else {
				$query['where'] .= $in_stock_clause;
			}
		} elseif ( $hide_out_of_stock ) {
			$query['where'] .= " AND {$this->lookup_table_name}.in_stock=1";
		}

		$search_query_sql = \WC_Query::get_main_search_query_sql();
		if ( $search_query_sql ) {
			$query['where'] .= ' AND ' . $search_query_sql;
		}

		$query['group_by'] = 'GROUP BY terms.term_id';
		$query['group_by'] = "GROUP BY {$this->lookup_table_name}.term_id";

		return $query;
	}

	/**
	 * Get the query for counting products by terms NOT using the product attributes lookup table.
	 *
	 * @param \WP_Tax_Query  $tax_query The current main tax query.
	 * @param \WP_Meta_Query $meta_query The current main meta query.
	 * @param string         $term_ids The term ids to include in the search.
	 * @return array An array of SQL query parts.
	 */
	private function get_product_counts_query_not_using_lookup_table( $tax_query, $meta_query, $term_ids ) {
		global $wpdb;

		$meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' );
		$tax_query_sql  = $tax_query->get_sql( $wpdb->posts, 'ID' );

		// Generate query.
		$query           = array();
		$query['select'] = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) AS term_count, terms.term_id AS term_count_id";
		$query['from']   = "FROM {$wpdb->posts}";
		$query['join']   = "
			INNER JOIN {$wpdb->term_relationships} AS term_relationships ON {$wpdb->posts}.ID = term_relationships.object_id
			INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id )
			INNER JOIN {$wpdb->terms} AS terms USING( term_id )
			" . $tax_query_sql['join'] . $meta_query_sql['join'];

		$term_ids_sql   = $this->get_term_ids_sql( $term_ids );
		$query['where'] = "
			WHERE {$wpdb->posts}.post_type IN ( 'product' )
			AND {$wpdb->posts}.post_status = 'publish'
			{$tax_query_sql['where']} {$meta_query_sql['where']}
			AND terms.term_id IN $term_ids_sql";

		$search_query_sql = \WC_Query::get_main_search_query_sql();
		if ( $search_query_sql ) {
			$query['where'] .= ' AND ' . $search_query_sql;
		}

		$query['group_by'] = 'GROUP BY terms.term_id';

		return $query;
	}

	/**
	 * Formats a list of term ids as "(id,id,id)".
	 *
	 * @param array $term_ids The list of terms to format.
	 * @return string The formatted list.
	 */
	private function get_term_ids_sql( $term_ids ) {
		return '(' . implode( ',', array_map( 'absint', $term_ids ) ) . ')';
	}
}
Internal/DownloadPermissionsAdjuster.php000064400000013527152333011040014536 0ustar00<?php
/**
 * DownloadPermissionsAdjuster class file.
 */

namespace Automattic\WooCommerce\Internal;

use Automattic\WooCommerce\Proxies\LegacyProxy;

defined( 'ABSPATH' ) || exit;

/**
 * Class to adjust download permissions on product save.
 */
class DownloadPermissionsAdjuster {

	/**
	 * The downloads data store to use.
	 *
	 * @var WC_Data_Store
	 */
	private $downloads_data_store;

	/**
	 * Class initialization, to be executed when the class is resolved by the container.
	 *
	 * @internal
	 */
	final public function init() {
		$this->downloads_data_store = wc_get_container()->get( LegacyProxy::class )->get_instance_of( \WC_Data_Store::class, 'customer-download' );
		add_action( 'adjust_download_permissions', array( $this, 'adjust_download_permissions' ), 10, 1 );
	}

	/**
	 * Schedule a download permissions adjustment for a product if necessary.
	 * This should be executed whenever a product is saved.
	 *
	 * @param \WC_Product $product The product to schedule a download permission adjustments for.
	 */
	public function maybe_schedule_adjust_download_permissions( \WC_Product $product ) {
		$children_ids = $product->get_children();
		if ( ! $children_ids ) {
			return;
		}

		$scheduled_action_args = array( $product->get_id() );

		$already_scheduled_actions =
			WC()->call_function(
				'as_get_scheduled_actions',
				array(
					'hook'   => 'adjust_download_permissions',
					'args'   => $scheduled_action_args,
					'status' => \ActionScheduler_Store::STATUS_PENDING,
				),
				'ids'
			);

		if ( empty( $already_scheduled_actions ) ) {
			WC()->call_function(
				'as_schedule_single_action',
				WC()->call_function( 'time' ) + 1,
				'adjust_download_permissions',
				$scheduled_action_args
			);
		}
	}

	/**
	 * Create additional download permissions for variations if necessary.
	 *
	 * When a simple downloadable product is converted to a variable product,
	 * existing download permissions are still present in the database but they don't apply anymore.
	 * This method creates additional download permissions for the variations based on
	 * the old existing ones for the main product.
	 *
	 * The procedure is as follows. For each existing download permission for the parent product,
	 * check if there's any variation offering the same file for download (the file URL, not name, is checked).
	 * If that is found, check if an equivalent permission exists (equivalent means for the same file and with
	 * the same order id and customer id). If no equivalent permission exists, create it.
	 *
	 * @param int $product_id The id of the product to check permissions for.
	 */
	public function adjust_download_permissions( int $product_id ) {
		$product = wc_get_product( $product_id );
		if ( ! $product ) {
			return;
		}

		$children_ids = $product->get_children();
		if ( ! $children_ids ) {
			return;
		}

		$parent_downloads = $this->get_download_files_and_permissions( $product );
		if ( ! $parent_downloads ) {
			return;
		}

		$children_with_downloads = array();
		foreach ( $children_ids as $child_id ) {
			$child                                = wc_get_product( $child_id );
			$children_with_downloads[ $child_id ] = $this->get_download_files_and_permissions( $child );
		}

		foreach ( $parent_downloads['permission_data_by_file_order_user'] as $parent_file_order_and_user => $parent_download_data ) {
			foreach ( $children_with_downloads as $child_id => $child_download_data ) {
				$file_url = $parent_download_data['file'];

				$must_create_permission =
					// The variation offers the same file as the parent for download...
					in_array( $file_url, array_keys( $child_download_data['download_ids_by_file_url'] ), true ) &&
					// ...but no equivalent download permission (same file URL, order id and user id) exists.
					! array_key_exists( $parent_file_order_and_user, $child_download_data['permission_data_by_file_order_user'] );

				if ( $must_create_permission ) {
					// The new child download permission is a copy of the parent's,
					// but with the product and download ids changed to match those of the variation.
					$new_download_data                = $parent_download_data['data'];
					$new_download_data['product_id']  = $child_id;
					$new_download_data['download_id'] = $child_download_data['download_ids_by_file_url'][ $file_url ];
					$this->downloads_data_store->create_from_data( $new_download_data );
				}
			}
		}
	}

	/**
	 * Get the existing downloadable files and download permissions for a given product.
	 * The returned value is an array with two keys:
	 *
	 * - download_ids_by_file_url: an associative array of file url => download_id.
	 * - permission_data_by_file_order_user: an associative array where key is "file_url:customer_id:order_id" and value is the full permission data set.
	 *
	 * @param \WC_Product $product The product to get the downloadable files and permissions for.
	 * @return array[] Information about the downloadable files and permissions for the product.
	 */
	private function get_download_files_and_permissions( \WC_Product $product ) {
		$result    = array(
			'permission_data_by_file_order_user' => array(),
			'download_ids_by_file_url'           => array(),
		);
		$downloads = $product->get_downloads();
		foreach ( $downloads as $download ) {
			$result['download_ids_by_file_url'][ $download->get_file() ] = $download->get_id();
		}

		$permissions = $this->downloads_data_store->get_downloads( array( 'product_id' => $product->get_id() ) );
		foreach ( $permissions as $permission ) {
			$permission_data = (array) $permission->data;
			if ( array_key_exists( $permission_data['download_id'], $downloads ) ) {
				$file = $downloads[ $permission_data['download_id'] ]->get_file();
				$data = array(
					'file' => $file,
					'data' => (array) $permission->data,
				);
				$result['permission_data_by_file_order_user'][ "${file}:${permission_data['user_id']}:${permission_data['order_id']}" ] = $data;
			}
		}

		return $result;
	}
}
Internal/Traits/AccessiblePrivateMethods.php000064400000017745152333011040015221 0ustar00<?php

namespace Automattic\WooCommerce\Internal\Traits;

use Automattic\WooCommerce\Utilities\ArrayUtil;

/**
 * This trait allows making private methods of a class accessible from outside.
 * This is useful to define hook handlers with the [$this, 'method'] or [__CLASS__, 'method'] syntax
 * without having to make the method public (and thus having to keep it forever for backwards compatibility).
 *
 * Example:
 *
 * class Foobar {
 *   use AccessiblePrivateMethods;
 *
 *   public function __construct() {
 *     self::add_action('some_action', [$this, 'handle_some_action']);
 *   }
 *
 *   public static function init() {
 *     self::add_filter('some_filter', [__CLASS__, 'handle_some_filter']);
 *   }
 *
 *   private function handle_some_action() {
 *   }
 *
 *   private static function handle_some_filter() {
 *   }
 * }
 *
 * For this to work the callback must be an array and the first element of the array must be either '$this', '__CLASS__',
 * or another instance of the same class; otherwise the method won't be marked as accessible
 * (but the corresponding WordPress 'add_action' and 'add_filter' functions will still be called).
 *
 * No special procedure is needed to remove hooks set up with these methods, the regular 'remove_action'
 * and 'remove_filter' functions provided by WordPress can be used as usual.
 */
trait AccessiblePrivateMethods {

	//phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
	/**
	 * List of instance methods marked as externally accessible.
	 *
	 * @var array
	 */
	private $_accessible_private_methods = array();

	/**
	 * List of static methods marked as externally accessible.
	 *
	 * @var array
	 */
	private static $_accessible_static_private_methods = array();
	//phpcs:enable PSR2.Classes.PropertyDeclaration.Underscore

	/**
	 * Register a WordPress action.
	 * If the callback refers to a private or protected instance method in this class, the method is marked as externally accessible.
	 *
	 * $callback can be a standard callable, or a string representing the name of a method in this class.
	 *
	 * @param string          $hook_name       The name of the action to add the callback to.
	 * @param callable|string $callback        The callback to be run when the action is called.
	 * @param int             $priority        Optional. Used to specify the order in which the functions
	 *                                         associated with a particular action are executed.
	 *                                         Lower numbers correspond with earlier execution,
	 *                                         and functions with the same priority are executed
	 *                                         in the order in which they were added to the action. Default 10.
	 * @param int             $accepted_args   Optional. The number of arguments the function accepts. Default 1.
	 */
	protected static function add_action( string $hook_name, $callback, int $priority = 10, int $accepted_args = 1 ): void {
		self::process_callback_before_hooking( $callback );
		add_action( $hook_name, $callback, $priority, $accepted_args );
	}

	/**
	 * Register a WordPress filter.
	 * If the callback refers to a private or protected instance method in this class, the method is marked as externally accessible.
	 *
	 * $callback can be a standard callable, or a string representing the name of a method in this class.
	 *
	 * @param string          $hook_name       The name of the filter to add the callback to.
	 * @param callable|string $callback        The callback to be run when the filter is called.
	 * @param int             $priority        Optional. Used to specify the order in which the functions
	 *                                         associated with a particular filter are executed.
	 *                                         Lower numbers correspond with earlier execution,
	 *                                         and functions with the same priority are executed
	 *                                         in the order in which they were added to the filter. Default 10.
	 * @param int             $accepted_args   Optional. The number of arguments the function accepts. Default 1.
	 */
	protected static function add_filter( string $hook_name, $callback, int $priority = 10, int $accepted_args = 1 ): void {
		self::process_callback_before_hooking( $callback );
		add_filter( $hook_name, $callback, $priority, $accepted_args );
	}

	/**
	 * Do the required processing to a callback before invoking the WordPress 'add_action' or 'add_filter' function.
	 *
	 * @param callable $callback The callback to process.
	 * @return void
	 */
	protected static function process_callback_before_hooking( $callback ): void {
		if ( ! is_array( $callback ) || count( $callback ) < 2 ) {
			return;
		}

		$first_item = $callback[0];
		if ( __CLASS__ === $first_item ) {
			static::mark_static_method_as_accessible( $callback[1] );
		} elseif ( is_object( $first_item ) && get_class( $first_item ) === __CLASS__ ) {
			$first_item->mark_method_as_accessible( $callback[1] );
		}
	}

	/**
	 * Register a private or protected instance method of this class as externally accessible.
	 *
	 * @param string $method_name Method name.
	 * @return bool True if the method has been marked as externally accessible, false if the method doesn't exist.
	 */
	protected function mark_method_as_accessible( string $method_name ): bool {
		// Note that an "is_callable" check would be useless here:
		// "is_callable" always returns true if the class implements __call.
		if ( method_exists( $this, $method_name ) ) {
			$this->_accessible_private_methods[ $method_name ] = $method_name;
			return true;
		}

		return false;
	}

	/**
	 * Register a private or protected static method of this class as externally accessible.
	 *
	 * @param string $method_name Method name.
	 * @return bool True if the method has been marked as externally accessible, false if the method doesn't exist.
	 */
	protected static function mark_static_method_as_accessible( string $method_name ): bool {
		if ( method_exists( __CLASS__, $method_name ) ) {
			static::$_accessible_static_private_methods[ $method_name ] = $method_name;
			return true;
		}

		return false;
	}

	/**
	 * Undefined/inaccessible instance method call handler.
	 *
	 * @param string $name Called method name.
	 * @param array  $arguments Called method arguments.
	 * @return mixed
	 * @throws \Error The called instance method doesn't exist or is private/protected and not marked as externally accessible.
	 */
	public function __call( $name, $arguments ) {
		if ( isset( $this->_accessible_private_methods[ $name ] ) ) {
			return call_user_func_array( array( $this, $name ), $arguments );
		} elseif ( is_callable( array( 'parent', '__call' ) ) ) {
			return parent::__call( $name, $arguments );
		} elseif ( method_exists( $this, $name ) ) {
			throw new \Error( 'Call to private method ' . get_class( $this ) . '::' . $name );
		} else {
			throw new \Error( 'Call to undefined method ' . get_class( $this ) . '::' . $name );
		}
	}

	/**
	 * Undefined/inaccessible static method call handler.
	 *
	 * @param string $name Called method name.
	 * @param array  $arguments Called method arguments.
	 * @return mixed
	 * @throws \Error The called static method doesn't exist or is private/protected and not marked as externally accessible.
	 */
	public static function __callStatic( $name, $arguments ) {
		if ( isset( static::$_accessible_static_private_methods[ $name ] ) ) {
			return call_user_func_array( array( __CLASS__, $name ), $arguments );
		} elseif ( is_callable( array( 'parent', '__callStatic' ) ) ) {
			return parent::__callStatic( $name, $arguments );
		} elseif ( 'add_action' === $name || 'add_filter' === $name ) {
			$proper_method_name = 'add_static_' . substr( $name, 4 );
			throw new \Error( __CLASS__ . '::' . $name . " can't be called statically, did you mean '$proper_method_name'?" );
		} elseif ( method_exists( __CLASS__, $name ) ) {
			throw new \Error( 'Call to private method ' . __CLASS__ . '::' . $name );
		} else {
			throw new \Error( 'Call to undefined method ' . __CLASS__ . '::' . $name );
		}
	}
}
Internal/WCCom/ConnectionHelper.php000064400000001216152333011040013230 0ustar00<?php
/**
 * Helpers for managing connection to WooCommerce.com.
 */

namespace Automattic\WooCommerce\Internal\WCCom;

defined( 'ABSPATH' ) || exit;

/**
 * Class WCConnectionHelper.
 *
 * Helpers for managing connection to WooCommerce.com.
 */
final class ConnectionHelper {
	/**
	 * Check if WooCommerce.com account is connected.
	 *
	 * @since 4.4.0
	 * @return bool Whether account is connected.
	 */
	public static function is_connected() {
		$helper_options    = get_option( 'woocommerce_helper_data', array() );
		if ( array_key_exists( 'auth', $helper_options ) && ! empty( $helper_options['auth'] ) ) {
			return true;
		}
		return false;
	}
}
Internal/ProductDownloads/ApprovedDirectories/Admin/SyncUI.php000064400000010246152333011040020476 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin;

use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchronize;
use Automattic\WooCommerce\Internal\Utilities\Users;

/**
 * Adds tools to the Status > Tools page that can be used to (re-)initiate or stop a synchronization process
 * for Approved Download Directories.
 */
class SyncUI {
	/**
	 * The active register of approved directories.
	 *
	 * @var Register
	 */
	private $register;

	/**
	 * Sets up UI controls for product download URLs.
	 *
	 * @internal
	 *
	 * @param Register $register Register of approved directories.
	 */
	final public function init( Register $register ) {
		$this->register = $register;
	}

	/**
	 * Performs any work needed to add hooks and otherwise integrate with the wider system,
	 * except in the case where the current user is not a site administrator, no hooks will
	 * be initialized.
	 */
	final public function init_hooks() {
		if ( ! Users::is_site_administrator() ) {
			return;
		}

		add_filter( 'woocommerce_debug_tools', array( $this, 'add_tools' ) );
	}

	/**
	 * Adds Approved Directory list-related entries to the tools page.
	 *
	 * @param array $tools Admin tool definitions.
	 *
	 * @return array
	 */
	public function add_tools( array $tools ): array {
		$sync = wc_get_container()->get( Synchronize::class );

		if ( ! $sync->in_progress() ) {
			// Provide tools to trigger a fresh scan (migration) and to clear the Approved Directories list.
			$tools['approved_directories_sync'] = array(
				'name'             => __( 'Synchronize approved download directories', 'woocommerce' ),
				'desc'             => __( 'Updates the list of Approved Product Download Directories. Note that triggering this tool does not impact whether the Approved Download Directories list is enabled or not.', 'woocommerce' ),
				'button'           => __( 'Update', 'woocommerce' ),
				'callback'         => array( $this, 'trigger_sync' ),
				'requires_refresh' => true,
			);

			$tools['approved_directories_clear'] = array(
				'name'             => __( 'Empty the approved download directories list', 'woocommerce' ),
				'desc'             => __( 'Removes all existing entries from the Approved Product Download Directories list.', 'woocommerce' ),
				'button'           => __( 'Clear', 'woocommerce' ),
				'callback'         => array( $this, 'clear_existing_entries' ),
				'requires_refresh' => true,
			);
		} else {
			// Or if a scan (migration) is already in progress, offer a means of cancelling it.
			$tools['cancel_directories_scan'] = array(
				'name'     => __( 'Cancel synchronization of approved directories', 'woocommerce' ),
				'desc'     => sprintf(
				/* translators: %d is an integer between 0-100 representing the percentage complete of the current scan. */
					__( 'The Approved Product Download Directories list is currently being synchronized with the product catalog (%d%% complete). If you need to, you can cancel it.', 'woocommerce' ),
					$sync->get_progress()
				),
				'button'   => __( 'Cancel', 'woocommerce' ),
				'callback' => array( $this, 'cancel_sync' ),
			);
		}

		return $tools;
	}

	/**
	 * Triggers a new migration.
	 */
	public function trigger_sync() {
		$this->security_check();
		wc_get_container()->get( Synchronize::class )->start();
	}

	/**
	 * Clears all existing rules from the Approved Directories list.
	 */
	public function clear_existing_entries() {
		$this->security_check();
		$this->register->delete_all();
	}

	/**
	 * If a migration is in progress, this will attempt to cancel it.
	 */
	public function cancel_sync() {
		$this->security_check();
		wc_get_logger()->log( 'info', __( 'Approved Download Directories sync: scan has been cancelled.', 'woocommerce' ) );
		wc_get_container()->get( Synchronize::class )->stop();
	}

	/**
	 * Makes sure the user has appropriate permissions and that we have a valid nonce.
	 */
	private function security_check() {
		if ( ! Users::is_site_administrator() ) {
			wp_die( esc_html__( 'You do not have permission to modify the list of approved directories for product downloads.', 'woocommerce' ) );
		}
	}
}
Internal/ProductDownloads/ApprovedDirectories/Admin/Table.php000064400000023766152333011040020366 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin;

use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\StoredUrl;
use WP_List_Table;
use WP_Screen;

/**
 * Admin list table used to render our current list of approved directories.
 */
class Table extends WP_List_Table {
	/**
	 * Initialize the webhook table list.
	 */
	public function __construct() {
		parent::__construct(
			array(
				'singular' => 'url',
				'plural'   => 'urls',
				'ajax'     => false,
			)
		);

		add_filter( 'manage_woocommerce_page_wc-settings_columns', array( $this, 'get_columns' ) );
		$this->items_per_page();
		set_screen_options();
	}

	/**
	 * Sets up an items-per-page control.
	 */
	private function items_per_page() {
		add_screen_option(
			'per_page',
			array(
				'default' => 20,
				'option'  => 'edit_approved_directories_per_page',
			)
		);

		add_filter( 'set_screen_option_edit_approved_directories_per_page', array( $this, 'set_items_per_page' ), 10, 3 );
	}

	/**
	 * Saves the items-per-page setting.
	 *
	 * @param mixed  $default The default value.
	 * @param string $option  The option being configured.
	 * @param int    $value   The submitted option value.
	 *
	 * @return mixed
	 */
	public function set_items_per_page( $default, string $option, int $value ) {
		return 'edit_approved_directories_per_page' === $option ? absint( $value ) : $default;
	}

	/**
	 * No items found text.
	 */
	public function no_items() {
		esc_html_e( 'No approved directory URLs found.', 'woocommerce' );
	}

	/**
	 * Displays the list of views available on this table.
	 */
	public function render_views() {
		$register = wc_get_container()->get( Register::class );

		$enabled_count  = $register->count( true );
		$disabled_count = $register->count( false );
		$all_count      = $enabled_count + $disabled_count;
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$selected_view = isset( $_REQUEST['view'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['view'] ) ) : 'all';

		$all_url   = esc_url( add_query_arg( 'view', 'all', $this->get_base_url() ) );
		$all_class = 'all' === $selected_view ? 'class="current"' : '';
		$all_text  = sprintf(
			/* translators: %s is the count of approved directory list entries. */
			_nx(
				'All <span class="count">(%s)</span>',
				'All <span class="count">(%s)</span>',
				$all_count,
				'Approved product download directory views',
				'woocommerce'
			),
			$all_count
		);

		$enabled_url   = esc_url( add_query_arg( 'view', 'enabled', $this->get_base_url() ) );
		$enabled_class = 'enabled' === $selected_view ? 'class="current"' : '';
		$enabled_text  = sprintf(
			/* translators: %s is the count of enabled approved directory list entries. */
			_nx(
				'Enabled <span class="count">(%s)</span>',
				'Enabled <span class="count">(%s)</span>',
				$enabled_count,
				'Approved product download directory views',
				'woocommerce'
			),
			$enabled_count
		);

		$disabled_url   = esc_url( add_query_arg( 'view', 'disabled', $this->get_base_url() ) );
		$disabled_class = 'disabled' === $selected_view ? 'class="current"' : '';
		$disabled_text  = sprintf(
			/* translators: %s is the count of disabled directory list entries. */
			_nx(
				'Disabled <span class="count">(%s)</span>',
				'Disabled <span class="count">(%s)</span>',
				$disabled_count,
				'Approved product download directory views',
				'woocommerce'
			),
			$disabled_count
		);

		$views = array(
			'all'      => "<a href='{$all_url}' {$all_class}>{$all_text}</a>",
			'enabled'  => "<a href='{$enabled_url}' {$enabled_class}>{$enabled_text}</a>",
			'disabled' => "<a href='{$disabled_url}' {$disabled_class}>{$disabled_text}</a>",
		);

		$this->screen->render_screen_reader_content( 'heading_views' );

		echo '<ul class="subsubsub list-table-filters">';
		foreach ( $views as $slug => $view ) {
			$views[ $slug ] = "<li class='{$slug}'>{$view}";
		}
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo implode( ' | </li>', $views ) . "</li>\n";
		echo '</ul>';
	}

	/**
	 * Get list columns.
	 *
	 * @return array
	 */
	public function get_columns() {
		return array(
			'cb'    => '<input type="checkbox" />',
			'title' => _x( 'URL', 'Approved product download directories', 'woocommerce' ),
			'enabled' => _x( 'Enabled', 'Approved product download directories', 'woocommerce' ),
		);
	}

	/**
	 * Checklist column, used for selecting items for processing by a bulk action.
	 *
	 * @param StoredUrl $item The approved directory information for the current row.
	 *
	 * @return string
	 */
	public function column_cb( $item ) {
		return sprintf( '<input type="checkbox" name="%1$s[]" value="%2$s" />', esc_attr( $this->_args['singular'] ), esc_attr( $item->get_id() ) );
	}

	/**
	 * URL column.
	 *
	 * @param StoredUrl $item The approved directory information for the current row.
	 *
	 * @return string
	 */
	public function column_title( $item ) {
		$id      = (int) $item->get_id();
		$url     = esc_html( $item->get_url() );
		$enabled = $item->is_enabled();

		$edit_url            = esc_url( $this->get_action_url( 'edit', $id ) );
		$enable_disable_url  = esc_url( $enabled ? $this->get_action_url( 'disable', $id ) : $this->get_action_url( 'enable', $id ) );
		$enable_disable_text = esc_html( $enabled ? __( 'Disable', 'woocommerce' ) : __( 'Enable', 'woocommerce' ) );
		$delete_url          = esc_url( $this->get_action_url( 'delete', $id ) );
		$edit_link           = "<a href='{$edit_url}'>" . esc_html_x( 'Edit', 'Product downloads list', 'woocommerce' ) . '</a>';
		$enable_disable_link = "<a href='{$enable_disable_url}'>{$enable_disable_text}</a>";
		$delete_link         = "<a href='{$delete_url}' class='submitdelete wc-confirm-delete'>" . esc_html_x( 'Delete permanently', 'Product downloads list', 'woocommerce' ) . '</a>';
		$url_link            = "<a href='{$edit_url}'>{$url}</a>";

		return "
			<strong>{$url_link}</strong>
			<div class='row-actions'>
				<span class='id'>ID: {$id}</span> |
				<span class='edit'>{$edit_link}</span> |
				<span class='enable-disable'>{$enable_disable_link}</span> |
				<span class='delete'><a class='submitdelete'>{$delete_link}</a></span>
			</div>
		";
	}

	/**
	 * Rule-is-enabled column.
	 *
	 * @param StoredUrl $item The approved directory information for the current row.
	 *
	 * @return string
	 */
	public function column_enabled( StoredUrl $item ): string {
		return $item->is_enabled()
			? '<mark class="yes" title="' . esc_html__( 'Enabled', 'woocommerce' ) . '"><span class="dashicons dashicons-yes"></span></mark>'
			: '<mark class="no" title="' . esc_html__( 'Disabled', 'woocommerce' ) . '">&ndash;</mark>';
	}

	/**
	 * Get bulk actions.
	 *
	 * @return array
	 */
	protected function get_bulk_actions() {
		return array(
			'enable'  => __( 'Enable rule', 'woocommerce' ),
			'disable' => __( 'Disable rule', 'woocommerce' ),
			'delete'  => __( 'Delete permanently', 'woocommerce' ),
		);
	}

	/**
	 * Builds an action URL (ie, to edit or delete a row).
	 *
	 * @param string $action       The action to be created.
	 * @param int    $id           The ID that is the subject of the action.
	 * @param string $nonce_action Action used to add a nonce to the URL.
	 *
	 * @return string
	 */
	public function get_action_url( string $action, int $id, string $nonce_action = 'modify_approved_directories' ): string {
		return add_query_arg(
			array(
				'check'  => wp_create_nonce( $nonce_action ),
				'action' => $action,
				'url'    => $id,
			),
			$this->get_base_url()
		);
	}

	/**
	 * Supplies the 'base' admin URL for this admin table.
	 *
	 * @return string
	 */
	public function get_base_url(): string {
		return add_query_arg(
			array(
				'page'    => 'wc-settings',
				'tab'     => 'products',
				'section' => 'download_urls',
			),
			admin_url( 'admin.php' )
		);
	}

	/**
	 * Generate the table navigation above or below the table.
	 * Included to remove extra nonce input.
	 *
	 * @param string $which The location of the extra table nav markup: 'top' or 'bottom'.
	 */
	protected function display_tablenav( $which ) {
		$directories = wc_get_container()->get( Register::class );
		echo '<div class="tablenav ' . esc_attr( $which ) . '">';

		if ( $this->has_items() ) {
			echo '<div class="alignleft actions bulkactions">';
			$this->bulk_actions( $which );

			if ( $directories->count( false ) > 0 ) {
				echo '<a href="' . esc_url( $this->get_action_url( 'enable-all', 0 ) ) . '" class="wp-core-ui button">' . esc_html_x( 'Enable All', 'Approved product download directories', 'woocommerce' ) . '</a> ';
			}

			if ( $directories->count( true ) > 0 ) {
				echo '<a href="' . esc_url( $this->get_action_url( 'disable-all', 0 ) ) . '" class="wp-core-ui button">' . esc_html_x( 'Disable All', 'Approved product download directories', 'woocommerce' ) . '</a>';
			}

			echo '</div>';
		}

		$this->pagination( $which );
		echo '<br class="clear" />';
		echo '</div>';
	}

	/**
	 * Prepare table list items.
	 */
	public function prepare_items() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		$current_page = $this->get_pagenum();
		$per_page     = $this->get_items_per_page( 'edit_approved_directories_per_page' );
		$search       = sanitize_text_field( wp_unslash( $_REQUEST['s'] ?? '' ) );

		switch ( $_REQUEST['view'] ?? '' ) {
			case 'enabled':
				$enabled = true;
				break;

			case 'disabled':
				$enabled = false;
				break;

			default:
				$enabled = null;
				break;
		}
		// phpcs:enable

		$approved_directories = wc_get_container()->get( Register::class )->list(
			array(
				'page'     => $current_page,
				'per_page' => $per_page,
				'search'   => $search,
				'enabled'  => $enabled,
			)
		);

		$this->items = $approved_directories['approved_directories'];

		// Set the pagination.
		$this->set_pagination_args(
			array(
				'total_items' => $approved_directories['total_urls'],
				'total_pages' => $approved_directories['total_pages'],
				'per_page'    => $per_page,
			)
		);
	}
}
Internal/ProductDownloads/ApprovedDirectories/Admin/UI.php000064400000035040152333011040017640 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin;

use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register;
use Automattic\WooCommerce\Internal\Utilities\Users;
use Exception;
use WC_Admin_Settings;

/**
 * Manages user interactions for product download URL safety.
 */
class UI {
	/**
	 * The active register of approved directories.
	 *
	 * @var Register
	 */
	private $register;

	/**
	 * The WP_List_Table instance used to display approved directories.
	 *
	 * @var Table
	 */
	private $table;

	/**
	 * Sets up UI controls for product download URLs.
	 *
	 * @internal
	 *
	 * @param Register $register Register of approved directories.
	 */
	final public function init( Register $register ) {
		$this->register = $register;
	}

	/**
	 * Performs any work needed to add hooks and otherwise integrate with the wider system,
	 * except in the case where the current user is not a site administrator, no hooks will
	 * be initialized.
	 */
	final public function init_hooks() {
		if ( ! Users::is_site_administrator() ) {
			return;
		}

		add_filter( 'woocommerce_get_sections_products', array( $this, 'add_section' ) );
		add_action( 'load-woocommerce_page_wc-settings', array( $this, 'setup' ) );
		add_action( 'woocommerce_settings_products', array( $this, 'render' ) );
	}

	/**
	 * Injects our new settings section (when approved directory rules are disabled, it will not show).
	 *
	 * @param array $sections Other admin settings sections.
	 *
	 * @return array
	 */
	public function add_section( array $sections ): array {
		$sections['download_urls'] = __( 'Approved download directories', 'woocommerce' );
		return $sections;
	}

	/**
	 * Sets up the table, renders any notices and processes actions as needed.
	 */
	public function setup() {
		if ( ! $this->is_download_urls_screen() ) {
			return;
		}

		$this->table = new Table();
		$this->admin_notices();
		$this->handle_search();
		$this->process_actions();
	}

	/**
	 * Renders the UI.
	 */
	public function render() {
		if ( null === $this->table || ! $this->is_download_urls_screen() ) {
			return;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( isset( $_REQUEST['action'] ) && 'edit' === $_REQUEST['action'] && isset( $_REQUEST['url'] ) ) {
			$this->edit_screen( (int) $_REQUEST['url'] );
			return;
		}
		// phpcs:enable

		// Show list table.
		$this->table->prepare_items();
		wp_nonce_field( 'modify_approved_directories', 'check' );
		$this->display_title();
		$this->table->render_views();
		$this->table->search_box( _x( 'Search', 'Approved Directory URLs', 'woocommerce' ), 'download_url_search' );
		$this->table->display();
	}

	/**
	 * Indicates if we are currently on the download URLs admin screen.
	 *
	 * @return bool
	 */
	private function is_download_urls_screen(): bool {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		return isset( $_GET['tab'] )
			&& 'products' === $_GET['tab']
			&& isset( $_GET['section'] )
			&& 'download_urls' === $_GET['section'];
		// phpcs:enable
	}

	/**
	 * Process bulk and single-row actions.
	 */
	private function process_actions() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$ids = isset( $_REQUEST['url'] ) ? array_map( 'absint', (array) $_REQUEST['url'] ) : array();

		if ( empty( $ids ) || empty( $_REQUEST['action'] ) ) {
			return;
		}

		$this->security_check();

		$action = sanitize_text_field( wp_unslash( $_REQUEST['action'] ) );

		switch ( $action ) {
			case 'edit':
				$this->process_edits( current( $ids ) );
				break;

			case 'delete':
			case 'enable':
			case 'disable':
				$this->process_bulk_actions( $ids, $action );
				break;

			case 'enable-all':
			case 'disable-all':
				$this->process_all_actions( $action );
				break;

			case 'turn-on':
			case 'turn-off':
				$this->process_on_off( $action );
				break;
		}
		// phpcs:enable
	}

	/**
	 * Support pagination across search results.
	 *
	 * In the context of the WC settings screen, form data is submitted by the post method: that poses
	 * a problem for the default WP_List_Table pagination logic which expects the search value to live
	 * as part of the URL query. This method is a simple shim to bridge the resulting gap.
	 */
	private function handle_search() {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		// phpcs:disable WordPress.Security.NonceVerification.Recommended

		// If a search value has not been POSTed, or if it was POSTed but is already equal to the
		// same value in the URL query, we need take no further action.
		if ( empty( $_POST['s'] ) || sanitize_text_field( wp_unslash( $_GET['s'] ?? '' ) ) === $_POST['s'] ) {
			return;
		}

		wp_safe_redirect(
			add_query_arg(
				array(
					'paged' => absint( $_GET['paged'] ?? 1 ),
					's'     => sanitize_text_field( wp_unslash( $_POST['s'] ) ),
				),
				$this->table->get_base_url()
			)
		);
		// phpcs:enable

		exit;
	}

	/**
	 * Handles updating or adding a new URL to the list of approved directories.
	 *
	 * @param int $url_id The ID of the rule to be edited/created. Zero if we are creating a new entry.
	 */
	private function process_edits( int $url_id ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		$url     = esc_url_raw( wp_unslash( $_POST['approved_directory_url'] ?? '' ) );
		$enabled = (bool) sanitize_text_field( wp_unslash( $_POST['approved_directory_enabled'] ?? '' ) );

		if ( empty( $url ) ) {
			return;
		}

		$redirect_url = add_query_arg( 'id', $url_id, $this->table->get_action_url( 'edit', $url_id ) );

		try {
			$upserted = 0 === $url_id
				? $this->register->add_approved_directory( $url, $enabled )
				: $this->register->update_approved_directory( $url_id, $url, $enabled );

			if ( is_integer( $upserted ) ) {
				$redirect_url = add_query_arg( 'url', $upserted, $redirect_url );
			}

			$redirect_url = add_query_arg( 'edit-status', 0 === $url_id ? 'added' : 'updated', $redirect_url );
		} catch ( Exception $e ) {
			$redirect_url = add_query_arg(
				array(
					'edit-status'   => 'failure',
					'submitted-url' => $url,
				),
				$redirect_url
			);
		}

		wp_safe_redirect( $redirect_url );
		exit;
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Processes actions that can be applied in bulk (requests to delete, enable
	 * or disable).
	 *
	 * @param int[]  $ids    The ID(s) to be updates.
	 * @param string $action The action to be applied.
	 */
	private function process_bulk_actions( array $ids, string $action ) {
		$deletes  = 0;
		$enabled  = 0;
		$disabled = 0;
		$register = wc_get_container()->get( Register::class );

		foreach ( $ids as $id ) {
			if ( 'delete' === $action && $register->delete_by_id( $id ) ) {
				$deletes++;
			} elseif ( 'enable' === $action && $register->enable_by_id( $id ) ) {
				$enabled++;
			} elseif ( 'disable' === $action && $register->disable_by_id( $id ) ) {
				$disabled ++;
			}
		}

		$fails    = count( $ids ) - $deletes - $enabled - $disabled;
		$redirect = $this->table->get_base_url();

		if ( $deletes ) {
			$redirect = add_query_arg( 'deleted-ids', $deletes, $redirect );
		} elseif ( $enabled ) {
			$redirect = add_query_arg( 'enabled-ids', $enabled, $redirect );
		} elseif ( $disabled ) {
			$redirect = add_query_arg( 'disabled-ids', $disabled, $redirect );
		}

		if ( $fails ) {
			$redirect = add_query_arg( 'bulk-fails', $fails, $redirect );
		}

		wp_safe_redirect( $redirect );
		exit;
	}

	/**
	 * Handles the enable/disable-all actions.
	 *
	 * @param string $action The action to be applied.
	 */
	private function process_all_actions( string $action ) {
		$register = wc_get_container()->get( Register::class );
		$redirect = $this->table->get_base_url();

		switch ( $action ) {
			case 'enable-all':
				$redirect = add_query_arg( 'enabled-all', (int) $register->enable_all(), $redirect );
				break;

			case 'disable-all':
				$redirect = add_query_arg( 'disabled-all', (int) $register->disable_all(), $redirect );
				break;
		}

		wp_safe_redirect( $redirect );
			exit;
}

	/**
	 * Handles turning on/off the entire approved download directory system (vs enabling
	 * and disabling of individual rules).
	 *
	 * @param string $action Whether the feature should be turned on or off.
	 */
	private function process_on_off( string $action ) {
		switch ( $action ) {
				case 'turn-on':
					$this->register->set_mode( Register::MODE_ENABLED );
					break;

			case 'turn-off':
				$this->register->set_mode( Register::MODE_DISABLED );
				break;
		}
	}

	/**
	 * Displays the screen title, etc.
	 */
	private function display_title() {
		$turn_on_off = $this->register->get_mode() === Register::MODE_ENABLED
			? '<a href="' . esc_url( $this->table->get_action_url( 'turn-off', 0 ) ) . '" class="page-title-action">' . esc_html_x( 'Stop Enforcing Rules', 'Approved product download directories', 'woocommerce' ) . '</a>'
			: '<a href="' . esc_url( $this->table->get_action_url( 'turn-on', 0 ) ) . '" class="page-title-action">' . esc_html_x( 'Start Enforcing Rules', 'Approved product download directories', 'woocommerce' ) . '</a>';

		?>
			<h2 class='wc-table-list-header'>
				<?php esc_html_e( 'Approved Download Directories', 'woocommerce' ); ?>
				<a href='<?php echo esc_url( $this->table->get_action_url( 'edit', 0 ) ); ?>' class='page-title-action'><?php esc_html_e( 'Add New', 'woocommerce' ); ?></a>
				<?php echo $turn_on_off; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
			</h2>
		<?php
	}

	/**
	 * Renders the editor screen for approved directory URLs.
	 *
	 * @param int $url_id The ID of the rule to be edited (may be zero for new rules).
	 */
	private function edit_screen( int $url_id ) {
		$this->security_check();
		$existing = $this->register->get_by_id( $url_id );

		if ( 0 !== $url_id && ! $existing ) {
			WC_Admin_Settings::add_error( _x( 'The provided ID was invalid.', 'Approved product download directories', 'woocommerce' ) );
			WC_Admin_Settings::show_messages();
			return;
		}

		$title = $existing
			? __( 'Edit Approved Directory', 'woocommerce' )
			: __( 'Add New Approved Directory', 'woocommerce' );

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$submitted    = sanitize_text_field( wp_unslash( $_GET['submitted-url'] ?? '' ) );
		$existing_url = $existing ? $existing->get_url() : '';
		$enabled      = $existing ? $existing->is_enabled() : true;
		// phpcs:enable

		?>
			<h2 class='wc-table-list-header'>
				<?php echo esc_html( $title ); ?>
				<?php if ( $existing ) : ?>
					<a href="<?php echo esc_url( $this->table->get_action_url( 'edit', 0 ) ); ?>" class="page-title-action"><?php esc_html_e( 'Add New', 'woocommerce' ); ?></a>
				<?php endif; ?>
				<a href="<?php echo esc_url( $this->table->get_base_url() ); ?> " class="page-title-action"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></a>
			</h2>
			<table class='form-table'>
				<tbody>
					<tr valign='top'>
						<th scope='row' class='titledesc'>
							<label for='approved_directory_url'> <?php echo esc_html_x( 'Directory URL', 'Approved product download directories', 'woocommerce' ); ?> </label>
						</th>
						<td class='forminp'>
							<input name='approved_directory_url' id='approved_directory_url' type='text' class='input-text regular-input' value='<?php echo esc_attr( empty( $submitted ) ? $existing_url : $submitted ); ?>'>
						</td>
					</tr>
					<tr valign='top'>
						<th scope='row' class='titledesc'>
							<label for='approved_directory_enabled'> <?php echo esc_html_x( 'Enabled', 'Approved product download directories', 'woocommerce' ); ?> </label>
						</th>
						<td class='forminp'>
							<input name='approved_directory_enabled' id='approved_directory_enabled' type='checkbox' value='1' <?php checked( true, $enabled ); ?>'>
						</td>
					</tr>
				</tbody>
			</table>
			<input name='id' id='approved_directory_id' type='hidden' value='{$url_id}'>
		<?php
	}

	/**
	 * Displays any admin notices that might be needed.
	 */
	private function admin_notices() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$successfully_deleted  = isset( $_GET['deleted-ids'] ) ? (int) $_GET['deleted-ids'] : 0;
		$successfully_enabled  = isset( $_GET['enabled-ids'] ) ? (int) $_GET['enabled-ids'] : 0;
		$successfully_disabled = isset( $_GET['disabled-ids'] ) ? (int) $_GET['disabled-ids'] : 0;
		$failed_updates        = isset( $_GET['bulk-fails'] ) ? (int) $_GET['bulk-fails'] : 0;
		$edit_status           = sanitize_text_field( wp_unslash( $_GET['edit-status'] ?? '' ) );
		$edit_url              = esc_attr( sanitize_text_field( wp_unslash( $_GET['submitted-url'] ?? '' ) ) );
		// phpcs:enable

		if ( $successfully_deleted ) {
			WC_Admin_Settings::add_message(
				sprintf(
					/* translators: %d: count */
					_n( '%d approved directory URL deleted.', '%d approved directory URLs deleted.', $successfully_deleted, 'woocommerce' ),
					$successfully_deleted
				)
			);
		} elseif ( $successfully_enabled ) {
			WC_Admin_Settings::add_message(
				sprintf(
				/* translators: %d: count */
					_n( '%d approved directory URL enabled.', '%d approved directory URLs enabled.', $successfully_enabled, 'woocommerce' ),
					$successfully_enabled
				)
			);
		} elseif ( $successfully_disabled ) {
			WC_Admin_Settings::add_message(
				sprintf(
				/* translators: %d: count */
					_n( '%d approved directory URL disabled.', '%d approved directory URLs disabled.', $successfully_disabled, 'woocommerce' ),
					$successfully_disabled
				)
			);
		}

		if ( $failed_updates ) {
			WC_Admin_Settings::add_error(
				sprintf(
					/* translators: %d: count */
					_n( '%d URL could not be updated.', '%d URLs could not be updated.', $failed_updates, 'woocommerce' ),
					$failed_updates
				)
			);
		}

		if ( 'added' === $edit_status ) {
			WC_Admin_Settings::add_message( __( 'URL was successfully added.', 'woocommerce' ) );
		}

		if ( 'updated' === $edit_status ) {
			WC_Admin_Settings::add_message( __( 'URL was successfully updated.', 'woocommerce' ) );
		}

		if ( 'failure' === $edit_status && ! empty( $edit_url ) ) {
			WC_Admin_Settings::add_error(
				sprintf(
					/* translators: %s is the submitted URL. */
					__( '"%s" could not be saved. Please review, ensure it is a valid URL and try again.', 'woocommerce' ),
					$edit_url
				)
			);
		}
	}

	/**
	 * Makes sure the user has appropriate permissions and that we have a valid nonce.
	 */
	private function security_check() {
		if ( ! Users::is_site_administrator() || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['check'] ?? '' ) ), 'modify_approved_directories' ) ) {
			wp_die( esc_html__( 'You do not have permission to modify the list of approved directories for product downloads.', 'woocommerce' ) );
		}
	}
}
Internal/ProductDownloads/ApprovedDirectories/Register.php000064400000033654152333011040020070 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories;

use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin\SyncUI;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin\UI;
use Automattic\WooCommerce\Internal\Utilities\URL;
use Automattic\WooCommerce\Internal\Utilities\URLException;

/**
 * Maintains and manages the list of approved directories, within which product downloads can
 * be stored.
 */
class Register {
	/**
	 * Used to indicate the current mode.
	 */
	private const MODES = array(
		self::MODE_DISABLED,
		self::MODE_ENABLED,
	);

	public const MODE_DISABLED  = 'disabled';
	public const MODE_ENABLED   = 'enabled';

	/**
	 * Name of the option used to store the current mode. See self::MODES for a
	 * list of acceptable values for the actual option.
	 *
	 * @var string
	 */
	private $mode_option = 'wc_downloads_approved_directories_mode';

	/**
	 * Sets up the approved directories sub-system.
	 *
	 * @internal
	 */
	final public function init() {
		add_action(
			'admin_init',
			function () {
				wc_get_container()->get( SyncUI::class )->init_hooks();
				wc_get_container()->get( UI::class )->init_hooks();
			}
		);

		add_action(
			'before_woocommerce_init',
			function() {
				if ( get_option( Synchronize::SYNC_TASK_PAGE ) > 0 ) {
					wc_get_container()->get( Synchronize::class )->init_hooks();
				}
			}
		);
	}

	/**
	 * Supplies the name of the database table used to store approved directories.
	 *
	 * @return string
	 */
	public function get_table(): string {
		global $wpdb;
		return $wpdb->prefix . 'wc_product_download_directories';
	}

	/**
	 * Returns a string indicating the current mode.
	 *
	 * May be one of: 'disabled', 'enabled', 'migrating'.
	 *
	 * @return string
	 */
	public function get_mode(): string {
		$current_mode = get_option( $this->mode_option, self::MODE_DISABLED );
		return in_array( $current_mode, self::MODES, true ) ? $current_mode : self::MODE_DISABLED;
	}

	/**
	 * Sets the mode. This effectively controls if approved directories are enforced or not.
	 *
	 * May be one of: 'disabled', 'enabled', 'migrating'.
	 *
	 * @param string $mode One of the values contained within self::MODES.
	 *
	 * @return bool
	 */
	public function set_mode( string $mode ): bool {
		if ( ! in_array( $mode, self::MODES, true ) ) {
			return false;
		}

		update_option( $this->mode_option, $mode );
		return get_option( $this->mode_option ) === $mode;
	}

	/**
	 * Adds a new URL path.
	 *
	 * On success (or if the URL was already added) returns the URL ID, or else
	 * returns boolean false.
	 *
	 * @throws URLException                 If the URL was invalid.
	 * @throws ApprovedDirectoriesException If the operation could not be performed.
	 *
	 * @param string $url     The URL of the approved directory.
	 * @param bool   $enabled If the rule is enabled.
	 *
	 * @return int
	 */
	public function add_approved_directory( string $url, bool $enabled = true ): int {
		$url      = $this->prepare_url_for_upsert( $url );
		$existing = $this->get_by_url( $url );

		if ( $existing ) {
			return $existing->get_id();
		}

		global $wpdb;
		$insert_fields = array(
			'url'     => $url,
			'enabled' => (int) $enabled,
		);

		if ( false !== $wpdb->insert( $this->get_table(), $insert_fields ) ) {
			return $wpdb->insert_id;
		}

		throw new ApprovedDirectoriesException( __( 'URL could not be added (probable database error).', 'woocommerce' ), ApprovedDirectoriesException::DB_ERROR );
	}

	/**
	 * Updates an existing approved directory.
	 *
	 * On success or if there is an existing entry for the same URL, returns true.
	 *
	 * @throws ApprovedDirectoriesException If the operation could not be performed.
	 * @throws URLException                 If the URL was invalid.
	 *
	 * @param int    $id      The ID of the approved directory to be updated.
	 * @param string $url     The new URL for the specified option.
	 * @param bool   $enabled If the rule is enabled.
	 *
	 * @return bool
	 */
	public function update_approved_directory( int $id, string $url, bool $enabled = true ): bool {
		$url           = $this->prepare_url_for_upsert( $url );
		$existing_path = $this->get_by_url( $url );

		// No need to go any further if the URL is already listed and nothing has changed.
		if ( $existing_path && $existing_path->get_url() === $url && $enabled === $existing_path->is_enabled() ) {
			return true;
		}

		global $wpdb;
		$fields = array(
			'url'     => $url,
			'enabled' => (int) $enabled,
		);

		if ( false === $wpdb->update( $this->get_table(), $fields, array( 'url_id' => $id ) ) ) {
			throw new ApprovedDirectoriesException( __( 'URL could not be updated (probable database error).', 'woocommerce' ), ApprovedDirectoriesException::DB_ERROR );
		}

		return true;
	}

	/**
	 * Indicates if the specified URL is already an approved directory.
	 *
	 * @param string $url The URL to check.
	 *
	 * @return bool
	 */
	public function approved_directory_exists( string $url ): bool {
		return (bool) $this->get_by_url( $url );
	}

	/**
	 * Returns the path identified by $id, or false if it does not exist.
	 *
	 * @param int $id The ID of the rule we are looking for.
	 *
	 * @return StoredUrl|false
	 */
	public function get_by_id( int $id ) {
		global $wpdb;

		$table = $this->get_table();

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE url_id = %d", array( $id ) ) );

		if ( ! $result ) {
			return false;
		}

		return new StoredUrl( $result->url_id, $result->url, $result->enabled );
	}

	/**
	 * Returns the path identified by $url, or false if it does not exist.
	 *
	 * @param string $url The URL of the rule we are looking for.
	 *
	 * @return StoredUrl|false
	 */
	public function get_by_url( string $url ) {
		global $wpdb;

		$table = $this->get_table();
		$url   = trailingslashit( $url );

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE url = %s", array( $url ) ) );

		if ( ! $result ) {
			return false;
		}

		return new StoredUrl( $result->url_id, $result->url, $result->enabled );
	}

	/**
	 * Indicates if the URL is within an approved directory. The approved directory must be enabled
	 * (it is possible for individual approved directories to be disabled).
	 *
	 * For instance, for 'https://storage.king/12345/ebook.pdf' to be valid then 'https://storage.king/12345'
	 * would need to be within our register.
	 *
	 * If the provided URL is a filepath it can be passed in without the 'file://' scheme.
	 *
	 * @throws URLException If the provided URL is badly formed.
	 *
	 * @param string $download_url The URL to check.
	 *
	 * @return bool
	 */
	public function is_valid_path( string $download_url ): bool {
		global $wpdb;

		$parent_directories = array();

		foreach ( ( new URL( $this->normalize_url( $download_url ) ) )->get_all_parent_urls() as $parent ) {
			$parent_directories[] = "'" . esc_sql( $parent ) . "'";
		}

		if ( empty( $parent_directories ) ) {
			return false;
		}

		$parent_directories = join( ',', $parent_directories );
		$table              = $this->get_table();

		// Look for a rule that matches the start of the download URL being tested. Since rules describe parent
		// directories, we also ensure it ends with a trailing slash.
		//
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$matches = (int) $wpdb->get_var(
			"
				SELECT COUNT(*)
				FROM   {$table}
				WHERE  enabled = 1
				       AND url IN ( {$parent_directories} )
			"
		);
		// phpcs:enable

		return $matches > 0;
	}

	/**
	 * Used when a URL string is prepared before potentially adding it to the database.
	 *
	 * It will be normalized and trailing-slashed; a length check will also be performed.
	 *
	 * @throws ApprovedDirectoriesException If the operation could not be performed.
	 * @throws URLException                 If the URL was invalid.
	 *
	 * @param string $url The string URL to be normalized and trailing-slashed.
	 *
	 * @return string
	 */
	private function prepare_url_for_upsert( string $url ): string {
		$url = trailingslashit( $this->normalize_url( $url ) );

		if ( mb_strlen( $url ) > 256 ) {
			throw new ApprovedDirectoriesException( __( 'Approved directory URLs cannot be longer than 256 characters.', 'woocommerce' ), ApprovedDirectoriesException::INVALID_URL );
		}

		return $url;
	}

	/**
	 * Normalizes the provided URL, by trimming whitespace per normal PHP conventions
	 * and removing any trailing slashes. If it lacks a scheme, the file scheme is
	 * assumed and prepended.
	 *
	 * @throws URLException If the URL is badly formed.
	 *
	 * @param string $url The URL to be normalized.
	 *
	 * @return string
	 */
	private function normalize_url( string $url ): string {
		$url = untrailingslashit( trim( $url ) );
		return ( new URL( $url ) )->get_url();
	}

	/**
	 * Lists currently approved directories.
	 *
	 * Returned array will have the following structure:
	 *
	 *     [
	 *         'total_urls'  => 12345,
	 *         'total_pages' => 123,
	 *         'urls'        => [],  # StoredUrl[]
	 *     ]
	 *
	 * @param array $args {
	 *     Controls pagination and ordering.
	 *
	 *     @type null|bool $enabled  Controls if only enabled (true), disabled (false) or all rules (null) should be listed.
	 *     @type string    $order    Ordering ('ASC' for ascending, 'DESC' for descending).
	 *     @type string    $order_by Field to order by (one of 'url_id' or 'url').
	 *     @type int       $page     The page of results to retrieve.
	 *     @type int       $per_page The number of results to retrieve per page.
	 *     @type string    $search   Term to search for.
	 * }
	 *
	 * @return array
	 */
	public function list( array $args ): array {
		global $wpdb;

		$args = array_merge(
			array(
				'enabled'  => null,
				'order'    => 'ASC',
				'order_by' => 'url',
				'page'     => 1,
				'per_page' => 20,
				'search'   => '',
			),
			$args
		);

		$table    = $this->get_table();
		$paths    = array();
		$order    = in_array( $args['order'], array( 'ASC', 'DESC' ), true ) ? $args['order'] : 'ASC';
		$order_by = in_array( $args['order_by'], array( 'url_id', 'url' ), true ) ? $args['order_by'] : 'url';
		$page     = absint( $args['page'] );
		$per_page = absint( $args['per_page'] );
		$enabled  = is_bool( $args['enabled'] ) ? $args['enabled'] : null;
		$search   = '%' . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . '%';

		if ( $page < 1 ) {
			$page = 1;
		}

		if ( $per_page < 1 ) {
			$per_page = 1;
		}

		$where     = array();
		$where_sql = '';

		if ( ! empty( $search ) ) {
			$where[] = $wpdb->prepare( 'url LIKE %s', $search );
		}

		if ( is_bool( $enabled ) ) {
			$where[] = 'enabled = ' . (int) $enabled;
		}

		if ( ! empty( $where ) ) {
			$where_sql = 'WHERE ' . join( ' AND ', $where );
		}

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"
					SELECT   url_id, url, enabled
					FROM     {$table}
					{$where_sql}
					ORDER BY {$order_by} {$order}
					LIMIT    %d, %d
				",
				( $page - 1 ) * $per_page,
				$per_page
			)
		);

		$total_rows = (int) $wpdb->get_var( "SELECT COUNT( * ) FROM {$table} {$where_sql}" );
		// phpcs:enable

		foreach ( $results as $single_result ) {
			$paths[] = new StoredUrl( $single_result->url_id, $single_result->url, $single_result->enabled );
		}

		return array(
			'total_urls'           => $total_rows,
			'total_pages'          => (int) ceil( $total_rows / $per_page ),
			'approved_directories' => $paths,
		);
	}

	/**
	 * Delete the approved directory identitied by the supplied ID.
	 *
	 * @param int $id The ID of the rule to be deleted.
	 *
	 * @return bool
	 */
	public function delete_by_id( int $id ): bool {
		global $wpdb;
		$table = $this->get_table();

		return (bool) $wpdb->delete( $table, array( 'url_id' => $id ) );
	}

	/**
	 * Delete the entirev approved directory list.
	 *
	 * @return bool
	 */
	public function delete_all(): bool {
		global $wpdb;
		$table = $this->get_table();
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return (bool) $wpdb->query( "DELETE FROM $table" );
	}

	/**
	 * Enable the approved directory identitied by the supplied ID.
	 *
	 * @param int $id The ID of the rule to be deleted.
	 *
	 * @return bool
	 */
	public function enable_by_id( int $id ): bool {
		global $wpdb;
		$table = $this->get_table();
		return (bool) $wpdb->update( $table, array( 'enabled' => 1 ), array( 'url_id' => $id ) );
	}

	/**
	 * Disable the approved directory identitied by the supplied ID.
	 *
	 * @param int $id The ID of the rule to be deleted.
	 *
	 * @return bool
	 */
	public function disable_by_id( int $id ): bool {
		global $wpdb;
		$table = $this->get_table();
		return (bool) $wpdb->update( $table, array( 'enabled' => 0 ), array( 'url_id' => $id ) );
	}

	/**
	 * Enables all Approved Download Directory rules in a single operation.
	 *
	 * @return bool
	 */
	public function enable_all(): bool {
		global $wpdb;
		$table = $this->get_table();
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return (bool) $wpdb->query( "UPDATE {$table} SET enabled = 1" );
	}

	/**
	 * Disables all Approved Download Directory rules in a single operation.
	 *
	 * @return bool
	 */
	public function disable_all(): bool {
		global $wpdb;
		$table = $this->get_table();
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return (bool) $wpdb->query( "UPDATE {$table} SET enabled = 0" );
	}

	/**
	 * Indicates the number of approved directories that are enabled (or disabled, if optional
	 * param $enabled is set to false).
	 *
	 * @param bool $enabled Controls whether enabled or disabled directory rules are counted.
	 *
	 * @return int
	 */
	public function count( bool $enabled = true ): int {
		global $wpdb;
		$table = $this->get_table();

		return (int) $wpdb->get_var(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT COUNT(*) FROM {$table} WHERE enabled = %d",
				$enabled ? 1 : 0
			)
		);
	}
}
Internal/ProductDownloads/ApprovedDirectories/Synchronize.php000064400000020374152333011040020612 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories;

use Exception;
use Automattic\WooCommerce\Internal\Utilities\URL;
use WC_Admin_Notices;
use WC_Product;
use WC_Queue_Interface;

/**
 * Ensures that any downloadable files have a corresponding entry in the Approved Product
 * Download Directories list.
 */
class Synchronize {
	/**
	 * Scheduled action hook used to facilitate scanning the product catalog for downloadable products.
	 */
	public const SYNC_TASK = 'woocommerce_download_dir_sync';

	/**
	 * The group under which synchronization tasks run (our standard 'woocommerce-db-updates' group).
	 */
	public const SYNC_TASK_GROUP = 'woocommerce-db-updates';

	/**
	 * Used to track progress throughout the sync process.
	 */
	public const SYNC_TASK_PAGE = 'wc_product_download_dir_sync_page';

	/**
	 * Used to record an estimation of progress on the current synchronization process. 0 means 0%,
	 * 100 means 100%.
	 *
	 * @param int
	 */
	public const SYNC_TASK_PROGRESS = 'wc_product_download_dir_sync_progress';

	/**
	 * Number of downloadable products to be processed in each atomic sync task.
	 */
	public const SYNC_TASK_BATCH_SIZE = 20;

	/**
	 * WC Queue.
	 *
	 * @var WC_Queue_Interface
	 */
	private $queue;

	/**
	 * Register of approved directories.
	 *
	 * @var Register
	 */
	private $register;

	/**
	 * Sets up our checks and controls for downloadable asset URLs, as appropriate for
	 * the current approved download directory mode.
	 *
	 * @internal
	 * @throws Exception If the WC_Queue instance cannot be obtained.
	 *
	 * @param Register $register The active approved download directories instance in use.
	 */
	final public function init( Register $register ) {
		$this->queue    = WC()->get_instance_of( WC_Queue_Interface::class );
		$this->register = $register;

	}

	/**
	 * Performs any work needed to add hooks and otherwise integrate with the wider system.
	 */
	final public function init_hooks() {
		add_action( self::SYNC_TASK, array( $this, 'run' ) );
	}

	/**
	 * Initializes the Approved Download Directories feature, typically following an update or
	 * during initial installation.
	 *
	 * @param bool $synchronize    Synchronize with existing product downloads. Not needed in a fresh installation.
	 * @param bool $enable_feature Enable (default) or disable the feature.
	 */
	public function init_feature( bool $synchronize = true, bool $enable_feature = true ) {
		try {
			$this->add_default_directories();

			if ( $synchronize ) {
				$this->start();
			}
		} catch ( Exception $e ) {
			wc_get_logger()->log( 'warning', __( 'It was not possible to synchronize download directories following the most recent update.', 'woocommerce' ) );
		}

		$this->register->set_mode(
			$enable_feature ? Register::MODE_ENABLED : Register::MODE_DISABLED
		);
	}

	/**
	 * By default we add the woocommerce_uploads directory (file path plus web URL) to the list
	 * of approved download directories.
	 *
	 * @throws Exception If the default directories cannot be added to the Approved List.
	 */
	public function add_default_directories() {
		$upload_dir = wp_get_upload_dir();
		$this->register->add_approved_directory( $upload_dir['basedir'] . '/woocommerce_uploads' );
		$this->register->add_approved_directory( $upload_dir['baseurl'] . '/woocommerce_uploads' );
	}

	/**
	 * Starts the synchronization process.
	 *
	 * @return bool
	 */
	public function start(): bool {
		if ( null !== $this->queue->get_next( self::SYNC_TASK ) ) {
			wc_get_logger()->log( 'warning', __( 'Synchronization of approved product download directories is already in progress.', 'woocommerce' ) );
			return false;
		}

		update_option( self::SYNC_TASK_PAGE, 1 );
		$this->queue->schedule_single( time(), self::SYNC_TASK, array(), self::SYNC_TASK_GROUP );
		wc_get_logger()->log( 'info', __( 'Approved Download Directories sync: new scan scheduled.', 'woocommerce' ) );
		return true;
	}

	/**
	 * Runs the syncronization task.
	 */
	public function run() {
		$products = $this->get_next_set_of_downloadable_products();

		foreach ( $products as $product ) {
			$this->process_product( $product );
		}

		// Detect if we have reached the end of the task.
		if ( count( $products ) < self::SYNC_TASK_BATCH_SIZE ) {
			wc_get_logger()->log( 'info', __( 'Approved Download Directories sync: scan is complete!', 'woocommerce' ) );
			$this->stop();
		} else {
			wc_get_logger()->log(
				'info',
				sprintf(
				/* translators: %1$d is the current batch in the synchronization task, %2$d is the percent complete. */
					__( 'Approved Download Directories sync: completed batch %1$d (%2$d%% complete).', 'woocommerce' ),
					(int) get_option( self::SYNC_TASK_PAGE, 2 ) - 1,
					$this->get_progress()
				)
			);
			$this->queue->schedule_single( time() + 1, self::SYNC_TASK, array(), self::SYNC_TASK_GROUP );
		}
	}

	/**
	 * Stops/cancels the current synchronization task.
	 */
	public function stop() {
		WC_Admin_Notices::add_notice( 'download_directories_sync_complete', true );
		delete_option( self::SYNC_TASK_PAGE );
		delete_option( self::SYNC_TASK_PROGRESS );
		$this->queue->cancel( self::SYNC_TASK );
	}

	/**
	 * Queries for the next batch of downloadable products, applying logic to ensure we only fetch those that actually
	 * have downloadable files (a downloadable product can be created that does not have downloadable files and/or
	 * downloadable files can be removed from existing downloadable products).
	 *
	 * @return array
	 */
	private function get_next_set_of_downloadable_products(): array {
		$query_filter = function ( array $query ): array {
			$query['meta_query'][] = array(
				'key'     => '_downloadable_files',
				'compare' => 'EXISTS',
			);

			return $query;
		};

		$page = (int) get_option( self::SYNC_TASK_PAGE, 1 );
		add_filter( 'woocommerce_product_data_store_cpt_get_products_query', $query_filter );

		$products = wc_get_products(
			array(
				'limit'    => self::SYNC_TASK_BATCH_SIZE,
				'page'     => $page,
				'paginate' => true,
			)
		);

		remove_filter( 'woocommerce_product_data_store_cpt_get_products_query', $query_filter );
		$progress = $products->max_num_pages > 0 ? (int) ( ( $page / $products->max_num_pages ) * 100 ) : 1;
		update_option( self::SYNC_TASK_PAGE, $page + 1 );
		update_option( self::SYNC_TASK_PROGRESS, $progress );

		return $products->products;
	}

	/**
	 * Processes an individual downloadable product, adding the parent paths for any downloadable files to the
	 * Approved Download Directories list.
	 *
	 * Any such paths will be added with the disabled flag set, because we want a site administrator to review
	 * and approve first.
	 *
	 * @param WC_Product $product The product we wish to examine for downloadable file paths.
	 */
	private function process_product( WC_Product $product ) {
		$downloads = $product->get_downloads();

		foreach ( $downloads as $downloadable ) {
			$parent_url = _x( 'invalid URL', 'Approved product download URLs migration', 'woocommerce' );

			try {
				$download_file = $downloadable->get_file();

				/**
				 * Controls whether shortcodes should be resolved and validated using the Approved Download Directory feature.
				 *
				 * @param bool $should_validate
				 */
				if ( apply_filters( 'woocommerce_product_downloads_approved_directory_validation_for_shortcodes', true ) && 'shortcode' === $downloadable->get_type_of_file_path() ) {
					$download_file = do_shortcode( $download_file );
				}

				$parent_url = ( new URL( $download_file ) )->get_parent_url();
				$this->register->add_approved_directory( $parent_url, false );
			} catch ( Exception $e ) {
				wc_get_logger()->log(
					'error',
					sprintf(
					/* translators: %s is a URL, %d is a product ID. */
						__( 'Product download migration: %1$s (for product %1$d) could not be added to the list of approved download directories.', 'woocommerce' ),
						$parent_url,
						$product->get_id()
					)
				);
			}
		}
	}

	/**
	 * Indicates if a synchronization of product download directories is in progress.
	 *
	 * @return bool
	 */
	public function in_progress(): bool {
		return (bool) get_option( self::SYNC_TASK_PAGE, false );
	}

	/**
	 * Returns a value between 0 and 100 representing the percentage complete of the current sync.
	 *
	 * @return int
	 */
	public function get_progress(): int {
		return min( 100, max( 0, (int) get_option( self::SYNC_TASK_PROGRESS, 0 ) ) );
	}
}
Internal/ProductDownloads/ApprovedDirectories/StoredUrl.php000064400000002427152333011040020221 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories;

/**
 * Representation of an approved directory URL, bundling the ID and URL in a single entity.
 */
class StoredUrl {
	/**
	 * The approved directory ID.
	 *
	 * @var int
	 */
	private $id;

	/**
	 * The approved directory URL.
	 *
	 * @var string
	 */
	private $url;

	/**
	 * If the individual rule is enabled or disabled.
	 *
	 * @var bool
	 */
	private $enabled;

	/**
	 * Sets up the approved directory rule.
	 *
	 * @param int    $id      The approved directory ID.
	 * @param string $url     The approved directory URL.
	 * @param bool   $enabled Indicates if the approved directory rule is enabled.
	 */
	public function __construct( int $id, string $url, bool $enabled ) {
		$this->id      = $id;
		$this->url     = $url;
		$this->enabled = $enabled;
	}

	/**
	 * Supplies the ID of the approved directory.
	 *
	 * @return int
	 */
	public function get_id(): int {
		return $this->id;
	}

	/**
	 * Supplies the approved directory URL.
	 *
	 * @return string
	 */
	public function get_url(): string {
		return $this->url;
	}

	/**
	 * Indicates if this rule is enabled or not (rules can be temporarily disabled).
	 *
	 * @return bool
	 */
	public function is_enabled(): bool {
		return $this->enabled;
	}
}
Internal/ProductDownloads/ApprovedDirectories/ApprovedDirectoriesException.php000064400000000523152333011040024125 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories;

use Exception;

/**
 * Encapsulates a problem encountered while an operation relating to approved directories
 * was performed.
 */
class ApprovedDirectoriesException extends Exception {
	public const INVALID_URL = 1;
	public const DB_ERROR    = 2;
}
Internal/RestApiUtil.php000064400000013304152333011040011227 0ustar00<?php
/**
 * ApiUtil class file.
 */

namespace Automattic\WooCommerce\Internal;

/**
 * Helper methos for the REST API.
 *
 * Class ApiUtil
 *
 * @package Automattic\WooCommerce\Internal
 */
class RestApiUtil {

	/**
	 * Converts a create refund request from the public API format:
	 *
	 * [
	 *   "reason" => "",
	 *   "api_refund" => "x",
	 *   "api_restock" => "x",
	 *   "line_items" => [
	 *     "id" => "111",
	 *     "quantity" => 222,
	 *     "refund_total" => 333,
	 *     "refund_tax" => [
	 *       [
	 *          "id": "444",
	 *          "refund_total": 555
	 *       ],...
	 *   ],...
	 * ]
	 *
	 * ...to the internally used format:
	 *
	 * [
	 *   "reason" => null,      (if it's missing or any empty value, set as null)
	 *   "api_refund" => true,  (if it's missing or non-bool, set as "true")
	 *   "api_restock" => true, (if it's missing or non-bool, set as "true")
	 *   "line_items" => [      (convert sequential array to associative based on "id")
	 *     "111" => [
	 *       "qty" => 222,      (rename "quantity" to "qty")
	 *       "refund_total" => 333,
	 *       "refund_tax" => [  (convert sequential array to associative based on "id" and "refund_total)
	 *         "444" => 555,...
	 *       ],...
	 *   ]
	 * ]
	 *
	 * It also calculates the amount if missing and whenever possible, see maybe_calculate_refund_amount_from_line_items.
	 *
	 * The conversion is done in a way that if the request is already in the internal format,
	 * then nothing is changed for compatibility. For example, if the line items array
	 * is already an associative array or any of its elements
	 * is missing the "id" key, then the entire array is left unchanged.
	 * Same for the "refund_tax" array inside each line item.
	 *
	 * @param \WP_REST_Request $request The request to adjust.
	 */
	public static function adjust_create_refund_request_parameters( \WP_REST_Request &$request ) {
		if ( empty( $request['reason'] ) ) {
			$request['reason'] = null;
		}

		if ( ! is_bool( $request['api_refund'] ) ) {
			$request['api_refund'] = true;
		}

		if ( ! is_bool( $request['api_restock'] ) ) {
			$request['api_restock'] = true;
		}

		if ( empty( $request['line_items'] ) ) {
			$request['line_items'] = array();
		} else {
			$request['line_items'] = self::adjust_line_items_for_create_refund_request( $request['line_items'] );
		}

		if ( ! isset( $request['amount'] ) ) {
			$amount = self::calculate_refund_amount_from_line_items( $request );
			if ( null !== $amount ) {
				$request['amount'] = strval( $amount );
			}
		}
	}

	/**
	 * Calculate the "amount" parameter for the request based on the amounts found in line items.
	 * This will ONLY be possible if ALL of the following is true:
	 *
	 * - "line_items" in the request is a non-empty array.
	 * - All line items have a "refund_total" field with a numeric value.
	 * - All values inside "refund_tax" in all line items are a numeric value.
	 *
	 * The request is assumed to be in internal format already.
	 *
	 * @param \WP_REST_Request $request The request to maybe calculate the total amount for.
	 * @return number|null The calculated amount, or null if it can't be calculated.
	 */
	private static function calculate_refund_amount_from_line_items( $request ) {
		$line_items = $request['line_items'];

		if ( ! is_array( $line_items ) || empty( $line_items ) ) {
			return null;
		}

		$amount = 0;

		foreach ( $line_items as $item ) {
			if ( ! isset( $item['refund_total'] ) || ! is_numeric( $item['refund_total'] ) ) {
				return null;
			}

			$amount += $item['refund_total'];

			if ( ! isset( $item['refund_tax'] ) ) {
				continue;
			}

			foreach ( $item['refund_tax'] as $tax ) {
				if ( ! is_numeric( $tax ) ) {
					return null;
				}
				$amount += $tax;
			}
		}

		return $amount;
	}

	/**
	 * Convert the line items of a refund request to internal format (see adjust_create_refund_request_parameters).
	 *
	 * @param array $line_items The line items to convert.
	 * @return array The converted line items.
	 */
	private static function adjust_line_items_for_create_refund_request( $line_items ) {
		if ( ! is_array( $line_items ) || empty( $line_items ) || self::is_associative( $line_items ) ) {
			return $line_items;
		}

		$new_array = array();
		foreach ( $line_items as $item ) {
			if ( ! isset( $item['id'] ) ) {
				return $line_items;
			}

			if ( isset( $item['quantity'] ) && ! isset( $item['qty'] ) ) {
				$item['qty'] = $item['quantity'];
			}
			unset( $item['quantity'] );

			if ( isset( $item['refund_tax'] ) ) {
				$item['refund_tax'] = self::adjust_taxes_for_create_refund_request_line_item( $item['refund_tax'] );
			}

			$id               = $item['id'];
			$new_array[ $id ] = $item;

			unset( $new_array[ $id ]['id'] );
		}

		return $new_array;
	}

	/**
	 * Adjust the taxes array from a line item in a refund request, see adjust_create_refund_parameters.
	 *
	 * @param array $taxes_array The array to adjust.
	 * @return array The adjusted array.
	 */
	private static function adjust_taxes_for_create_refund_request_line_item( $taxes_array ) {
		if ( ! is_array( $taxes_array ) || empty( $taxes_array ) || self::is_associative( $taxes_array ) ) {
			return $taxes_array;
		}

		$new_array = array();
		foreach ( $taxes_array as $item ) {
			if ( ! isset( $item['id'] ) || ! isset( $item['refund_total'] ) ) {
				return $taxes_array;
			}

			$id               = $item['id'];
			$refund_total     = $item['refund_total'];
			$new_array[ $id ] = $refund_total;
		}

		return $new_array;
	}

	/**
	 * Is an array sequential or associative?
	 *
	 * @param array $array The array to check.
	 * @return bool True if the array is associative, false if it's sequential.
	 */
	private static function is_associative( array $array ) {
		return array_keys( $array ) !== range( 0, count( $array ) - 1 );
	}
}
Internal/DataStores/Orders/OrdersTableDataStoreMeta.php000064400000001336152333011040017157 0ustar00<?php
/**
 * OrdersTableDataStoreMeta class file.
 */

namespace Automattic\WooCommerce\Internal\DataStores\Orders;

use Automattic\WooCommerce\Internal\DataStores\CustomMetaDataStore;

/**
 * Mimics a WP metadata (i.e. add_metadata(), get_metadata() and friends) implementation using a custom table.
 */
class OrdersTableDataStoreMeta extends CustomMetaDataStore {

	/**
	 * Returns the name of the table used for storage.
	 *
	 * @return string
	 */
	protected function get_table_name() {
		return OrdersTableDataStore::get_meta_table_name();
	}

	/**
	 * Returns the name of the field/column used for associating meta with objects.
	 *
	 * @return string
	 */
	protected function get_object_id_field() {
		return 'order_id';
	}

}
Internal/DataStores/Orders/CustomOrdersTableController.php000064400000052624152333011040020006 0ustar00<?php
/**
 * CustomOrdersTableController class file.
 */

namespace Automattic\WooCommerce\Internal\DataStores\Orders;

use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;

defined( 'ABSPATH' ) || exit;

/**
 * This is the main class that controls the custom orders tables feature. Its responsibilities are:
 *
 * - Allowing to enable and disable the feature while it's in development (show_feature method)
 * - Displaying UI components (entries in the tools page and in settings)
 * - Providing the proper data store for orders via 'woocommerce_order_data_store' hook
 *
 * ...and in general, any functionality that doesn't imply database access.
 */
class CustomOrdersTableController {

	use AccessiblePrivateMethods;

	/**
	 * The name of the option for enabling the usage of the custom orders tables
	 */
	public const CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION = 'woocommerce_custom_orders_table_enabled';

	/**
	 * The name of the option that tells that the authoritative table must be flipped once sync finishes.
	 */
	private const AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION = 'woocommerce_auto_flip_authoritative_table_roles';

	/**
	 * The name of the option that tells whether database transactions are to be used or not for data synchronization.
	 */
	public const USE_DB_TRANSACTIONS_OPTION = 'woocommerce_use_db_transactions_for_custom_orders_table_data_sync';

	/**
	 * The name of the option to store the transaction isolation level to use when database transactions are enabled.
	 */
	public const DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION = 'woocommerce_db_transactions_isolation_level_for_custom_orders_table_data_sync';

	public const DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL = 'REPEATABLE READ';

	/**
	 * The data store object to use.
	 *
	 * @var OrdersTableDataStore
	 */
	private $data_store;

	/**
	 * Refunds data store object to use.
	 *
	 * @var OrdersTableRefundDataStore
	 */
	private $refund_data_store;

	/**
	 * The data synchronizer object to use.
	 *
	 * @var DataSynchronizer
	 */
	private $data_synchronizer;

	/**
	 * The batch processing controller to use.
	 *
	 * @var BatchProcessingController
	 */
	private $batch_processing_controller;

	/**
	 * The features controller to use.
	 *
	 * @var FeaturesController
	 */
	private $features_controller;

	/**
	 * Class constructor.
	 */
	public function __construct() {
		$this->init_hooks();
	}

	/**
	 * Initialize the hooks used by the class.
	 */
	private function init_hooks() {
		self::add_filter( 'woocommerce_order_data_store', array( $this, 'get_orders_data_store' ), 999, 1 );
		self::add_filter( 'woocommerce_order-refund_data_store', array( $this, 'get_refunds_data_store' ), 999, 1 );
		self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 999, 1 );
		self::add_filter( 'woocommerce_get_sections_advanced', array( $this, 'get_settings_sections' ), 999, 1 );
		self::add_filter( 'woocommerce_get_settings_advanced', array( $this, 'get_settings' ), 999, 2 );
		self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
		self::add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 );
		self::add_filter( DataSynchronizer::PENDING_SYNCHRONIZATION_FINISHED_ACTION, array( $this, 'process_sync_finished' ), 10, 0 );
		self::add_action( 'woocommerce_update_options_advanced_custom_data_stores', array( $this, 'process_options_updated' ), 10, 0 );
		self::add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 );
	}

	/**
	 * Class initialization, invoked by the DI container.
	 *
	 * @internal
	 * @param OrdersTableDataStore       $data_store The data store to use.
	 * @param DataSynchronizer           $data_synchronizer The data synchronizer to use.
	 * @param OrdersTableRefundDataStore $refund_data_store The refund data store to use.
	 * @param BatchProcessingController  $batch_processing_controller The batch processing controller to use.
	 * @param FeaturesController         $features_controller The features controller instance to use.
	 */
	final public function init(
		OrdersTableDataStore $data_store,
		DataSynchronizer $data_synchronizer,
		OrdersTableRefundDataStore $refund_data_store,
		BatchProcessingController $batch_processing_controller,
		FeaturesController $features_controller ) {
		$this->data_store                  = $data_store;
		$this->data_synchronizer           = $data_synchronizer;
		$this->batch_processing_controller = $batch_processing_controller;
		$this->refund_data_store           = $refund_data_store;
		$this->features_controller         = $features_controller;
	}

	/**
	 * Checks if the feature is visible (so that dedicated entries will be added to the debug tools page).
	 *
	 * @return bool True if the feature is visible.
	 */
	public function is_feature_visible(): bool {
		return $this->features_controller->feature_is_enabled( 'custom_order_tables' );
	}

	/**
	 * Makes the feature visible, so that dedicated entries will be added to the debug tools page.
	 *
	 * This method shouldn't be used anymore, see the FeaturesController class.
	 */
	public function show_feature() {
		$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . __FUNCTION__;
		wc_doing_it_wrong(
			$class_and_method,
			__( "${class_and_method}: The visibility of the custom orders table feature is now handled by the WooCommerce features engine. See the FeaturesController class, or go to WooCommerce - Settings - Advanced - Features.", 'woocommerce' ),
			'7.0'
		);
	}

	/**
	 * Hides the feature, so that no entries will be added to the debug tools page.
	 *
	 * This method shouldn't be used anymore, see the FeaturesController class.
	 */
	public function hide_feature() {
		$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . __FUNCTION__;
		wc_doing_it_wrong(
			$class_and_method,
			__( "${class_and_method}: The visibility of the custom orders table feature is now handled by the WooCommerce features engine. See the FeaturesController class, or go to WooCommerce - Settings - Advanced - Features.", 'woocommerce' ),
			'7.0'
		);
	}

	/**
	 * Is the custom orders table usage enabled via settings?
	 * This can be true only if the feature is enabled and a table regeneration has been completed.
	 *
	 * @return bool True if the custom orders table usage is enabled
	 */
	public function custom_orders_table_usage_is_enabled(): bool {
		return $this->is_feature_visible() && get_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) === 'yes';
	}

	/**
	 * Gets the instance of the orders data store to use.
	 *
	 * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the woocommerce_order_data_store hook).
	 *
	 * @return \WC_Object_Data_Store_Interface|string The actual data store to use.
	 */
	private function get_orders_data_store( $default_data_store ) {
		return $this->get_data_store_instance( $default_data_store, 'order' );
	}

	/**
	 * Gets the instance of the refunds data store to use.
	 *
	 * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the woocommerce_order-refund_data_store hook).
	 *
	 * @return \WC_Object_Data_Store_Interface|string The actual data store to use.
	 */
	private function get_refunds_data_store( $default_data_store ) {
		return $this->get_data_store_instance( $default_data_store, 'order_refund' );
	}

	/**
	 * Gets the instance of a given data store.
	 *
	 * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the appropriate hooks).
	 * @param string                                 $type               The type of the data store to get.
	 *
	 * @return \WC_Object_Data_Store_Interface|string The actual data store to use.
	 */
	private function get_data_store_instance( $default_data_store, string $type ) {
		if ( $this->custom_orders_table_usage_is_enabled() ) {
			switch ( $type ) {
				case 'order_refund':
					return $this->refund_data_store;
				default:
					return $this->data_store;
			}
		} else {
			return $default_data_store;
		}
	}

	/**
	 * Add an entry to Status - Tools to create or regenerate the custom orders table,
	 * and also an entry to delete the table as appropriate.
	 *
	 * @param array $tools_array The array of tools to add the tool to.
	 * @return array The updated array of tools-
	 */
	private function add_initiate_regeneration_entry_to_tools_array( array $tools_array ): array {
		if ( ! $this->is_feature_visible() ) {
			return $tools_array;
		}

		if ( $this->data_synchronizer->check_orders_table_exists() ) {
			$tools_array['delete_custom_orders_table'] = array(
				'name'             => __( 'Delete the custom orders tables', 'woocommerce' ),
				'desc'             => sprintf(
					'<strong class="red">%1$s</strong> %2$s',
					__( 'Note:', 'woocommerce' ),
					__( 'This will delete the custom orders tables. The tables can be deleted only if they are not not in use (via Settings > Advanced > Custom data stores). You can create them again at any time with the "Create the custom orders tables" tool.', 'woocommerce' )
				),
				'requires_refresh' => true,
				'callback'         => function () {
					$this->delete_custom_orders_tables();
					return __( 'Custom orders tables have been deleted.', 'woocommerce' );
				},
				'button'           => __( 'Delete', 'woocommerce' ),
				'disabled'         => $this->custom_orders_table_usage_is_enabled(),
			);
		} else {
			$tools_array['create_custom_orders_table'] = array(
				'name'             => __( 'Create the custom orders tables', 'woocommerce' ),
				'desc'             => __( 'This tool will create the custom orders tables. Once created you can go to WooCommerce > Settings > Advanced > Custom data stores and configure the usage of the tables.', 'woocommerce' ),
				'requires_refresh' => true,
				'callback'         => function() {
					$this->create_custom_orders_tables();
					return __( 'Custom orders tables have been created. You can now go to WooCommerce > Settings > Advanced > Custom data stores.', 'woocommerce' );
				},
				'button'           => __( 'Create', 'woocommerce' ),
			);
		}

		return $tools_array;
	}

	/**
	 * Create the custom orders tables in response to the user pressing the tool button.
	 *
	 * @throws \Exception Can't create the tables.
	 */
	private function create_custom_orders_tables() {
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
		if ( ! isset( $_REQUEST['_wpnonce'] ) || wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) === false ) {
			throw new \Exception( 'Invalid nonce' );
		}

		if ( ! $this->is_feature_visible() ) {
			throw new \Exception( "Can't create the custom orders tables: the feature isn't enabled" );
		}

		$this->data_synchronizer->create_database_tables();
		update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
	}

	/**
	 * Delete the custom orders tables and any related options and data in response to the user pressing the tool button.
	 *
	 * @throws \Exception Can't delete the tables.
	 */
	private function delete_custom_orders_tables() {
		if ( $this->custom_orders_table_usage_is_enabled() ) {
			throw new \Exception( "Can't delete the custom orders tables: they are currently in use (via Settings > Advanced > Custom data stores)." );
		}

		delete_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION );
		$this->data_synchronizer->delete_database_tables();
	}

	/**
	 * Get the settings sections for the "Advanced" tab, with a "Custom data stores" section added if appropriate.
	 *
	 * @param array $sections The original settings sections array.
	 * @return array The updated settings sections array.
	 */
	private function get_settings_sections( array $sections ): array {
		if ( ! $this->is_feature_visible() ) {
			return $sections;
		}

		$sections['custom_data_stores'] = __( 'Custom data stores', 'woocommerce' );

		return $sections;
	}

	/**
	 * Get the settings for the "Custom data stores" section in the "Advanced" tab,
	 * with entries for managing the custom orders tables if appropriate.
	 *
	 * @param array  $settings The original settings array.
	 * @param string $section_id The settings section to get the settings for.
	 * @return array The updated settings array.
	 */
	private function get_settings( array $settings, string $section_id ): array {
		if ( ! $this->is_feature_visible() || $section_id !== 'custom_data_stores' ) {
			return $settings;
		}

		if ( $this->data_synchronizer->check_orders_table_exists() ) {
			$settings[] = array(
				'title' => __( 'Custom orders tables', 'woocommerce' ),
				'type'  => 'title',
				'id'    => 'cot-title',
				'desc'  => sprintf(
					/* translators: %1$s = <strong> tag, %2$s = </strong> tag. */
					__( '%1$sWARNING:%2$s This feature is currently under development and may cause database instability. For contributors only.', 'woocommerce' ),
					'<strong>',
					'</strong>'
				),
			);

			$sync_status     = $this->data_synchronizer->get_sync_status();
			$sync_is_pending = $sync_status['current_pending_count'] !== 0;

			$settings[] = array(
				'title'         => __( 'Data store for orders', 'woocommerce' ),
				'id'            => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
				'default'       => 'no',
				'type'          => 'radio',
				'options'       => array(
					'yes' => __( 'Use the WooCommerce orders tables', 'woocommerce' ),
					'no'  => __( 'Use the WordPress posts table', 'woocommerce' ),
				),
				'checkboxgroup' => 'start',
				'disabled'      => $sync_is_pending ? array( 'yes', 'no' ) : array(),
			);

			if ( $sync_is_pending ) {
				$initial_pending_count = $sync_status['initial_pending_count'];
				$current_pending_count = $sync_status['current_pending_count'];
				if ( $initial_pending_count ) {
					$text =
						sprintf(
							/* translators: %1$s=current number of orders pending sync, %2$s=initial number of orders pending sync */
							_n( 'There\'s %1$s order (out of a total of %2$s) pending sync!', 'There are %1$s orders (out of a total of %2$s) pending sync!', $current_pending_count, 'woocommerce' ),
							$current_pending_count,
							$initial_pending_count
						);
				} else {
					$text =
						/* translators: %s=initial number of orders pending sync */
						sprintf( _n( 'There\'s %s order pending sync!', 'There are %s orders pending sync!', $current_pending_count, 'woocommerce' ), $current_pending_count, 'woocommerce' );
				}

				if ( $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) ) ) {
					$text .= __( "<br/>Synchronization for these orders is currently in progress.<br/>The authoritative table can't be changed until sync completes.", 'woocommerce' );
				} else {
					$text .= __( "<br/>The authoritative table can't be changed until these orders are synchronized.", 'woocommerce' );
				}

				$settings[] = array(
					'type' => 'info',
					'id'   => 'cot-out-of-sync-warning',
					'css'  => 'color: #C00000',
					'text' => $text,
				);
			}

			$settings[] = array(
				'desc' => __( 'Keep the posts table and the orders tables synchronized', 'woocommerce' ),
				'id'   => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
				'type' => 'checkbox',
			);

			if ( $sync_is_pending ) {
				if ( $this->data_synchronizer->data_sync_is_enabled() ) {
					$message    = $this->custom_orders_table_usage_is_enabled() ?
						__( 'Switch to using the posts table as the authoritative data store for orders when sync finishes', 'woocommerce' ) :
						__( 'Switch to using the orders table as the authoritative data store for orders when sync finishes', 'woocommerce' );
					$settings[] = array(
						'desc' => $message,
						'id'   => self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION,
						'type' => 'checkbox',
					);
				}
			}

			$settings[] = array(
				'desc' => __( 'Use database transactions for the orders data synchronization', 'woocommerce' ),
				'id'   => self::USE_DB_TRANSACTIONS_OPTION,
				'type' => 'checkbox',
			);

			$isolation_level_names = self::get_valid_transaction_isolation_levels();
			$settings[]            = array(
				'desc'    => __( 'Database transaction isolation level to use', 'woocommerce' ),
				'id'      => self::DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION,
				'type'    => 'select',
				'options' => array_combine( $isolation_level_names, $isolation_level_names ),
				'default' => self::DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL,
			);
		} else {
			$settings[] = array(
				'title' => __( 'Custom orders tables', 'woocommerce' ),
				'type'  => 'title',
				'desc'  => sprintf(
					/* translators: %1$s = <em> tag, %2$s = </em> tag. */
					__( 'Create the tables first by going to %1$sWooCommerce > Status > Tools%2$s and running %1$sCreate the custom orders tables%2$s.', 'woocommerce' ),
					'<em>',
					'</em>'
				),
			);
		}

		$settings[] = array( 'type' => 'sectionend' );

		return $settings;
	}

	/**
	 * Get the valid database transaction isolation level names.
	 *
	 * @return string[]
	 */
	public static function get_valid_transaction_isolation_levels() {
		return array(
			'REPEATABLE READ',
			'READ COMMITTED',
			'READ UNCOMMITTED',
			'SERIALIZABLE',
		);
	}

	/**
	 * Handler for the individual setting updated hook.
	 *
	 * @param string $option Setting name.
	 * @param mixed  $old_value Old value of the setting.
	 * @param mixed  $value New value of the setting.
	 */
	private function process_updated_option( $option, $old_value, $value ) {
		if ( $option === DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION && $value === 'no' ) {
			$this->data_synchronizer->cleanup_synchronization_state();
		}
	}

	/**
	 * Handler for the setting pre-update hook.
	 * We use it to verify that authoritative orders table switch doesn't happen while sync is pending.
	 *
	 * @param mixed  $value New value of the setting.
	 * @param string $option Setting name.
	 * @param mixed  $old_value Old value of the setting.
	 *
	 * @throws \Exception Attempt to change the authoritative orders table while orders sync is pending.
	 */
	private function process_pre_update_option( $value, $option, $old_value ) {
		if ( $option !== self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION || $value === $old_value || $old_value === false ) {
			return $value;
		}

		/**
		 * Commenting out for better testability.
		$sync_is_pending = 0 !== $this->data_synchronizer->get_current_orders_pending_sync_count();
		if ( $sync_is_pending ) {
			throw new \Exception( "The authoritative table for orders storage can't be changed while there are orders out of sync" );
		}
		 */

		return $value;
	}

	/**
	 * Handler for the synchronization finished hook.
	 * Here we switch the authoritative table if needed.
	 */
	private function process_sync_finished() {
		if ( ! $this->auto_flip_authoritative_table_enabled() ) {
			return;
		}

		update_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' );

		if ( $this->custom_orders_table_usage_is_enabled() ) {
			update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
		} else {
			update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'yes' );
		}
	}

	/**
	 * Is the automatic authoritative table switch setting set?
	 *
	 * @return bool
	 */
	private function auto_flip_authoritative_table_enabled(): bool {
		return get_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ) === 'yes';
	}

	/**
	 * Handler for the all settings updated hook.
	 */
	private function process_options_updated() {
		$data_sync_is_enabled = $this->data_synchronizer->data_sync_is_enabled();

		// Disabling the sync implies disabling the automatic authoritative table switch too.
		if ( ! $data_sync_is_enabled && $this->auto_flip_authoritative_table_enabled() ) {
			update_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' );
		}

		// Enabling/disabling the sync implies starting/stopping it too, if needed.
		// We do this check here, and not in process_pre_update_option, so that if for some reason
		// the setting is enabled but no sync is in process, sync will start by just saving the
		// settings even without modifying them (and the opposite: sync will be stopped if for
		// some reason it was ongoing while it was disabled).
		if ( $data_sync_is_enabled ) {
			$this->batch_processing_controller->enqueue_processor( DataSynchronizer::class );
		} else {
			$this->batch_processing_controller->remove_processor( DataSynchronizer::class );
		}
	}

	/**
	 * Handler for the woocommerce_after_register_post_type post,
	 * registers the post type for placeholder orders.
	 *
	 * @return void
	 */
	private function register_post_type_for_order_placeholders(): void {
		wc_register_order_type(
			DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE,
			array(
				'public'                           => false,
				'exclude_from_search'              => true,
				'publicly_queryable'               => false,
				'show_ui'                          => false,
				'show_in_menu'                     => false,
				'show_in_nav_menus'                => false,
				'show_in_admin_bar'                => false,
				'show_in_rest'                     => false,
				'rewrite'                          => false,
				'query_var'                        => false,
				'can_export'                       => false,
				'supports'                         => array(),
				'capabilities'                     => array(),
				'exclude_from_order_count'         => true,
				'exclude_from_order_views'         => true,
				'exclude_from_order_reports'       => true,
				'exclude_from_order_sales_reports' => true,
			)
		);
	}
}
Internal/DataStores/Orders/OrdersTableSearchQuery.php000064400000007234152333011040016720 0ustar00<?php

namespace Automattic\WooCommerce\Internal\DataStores\Orders;

use Exception;

/**
 * Creates the join and where clauses needed to perform an order search using Custom Order Tables.
 *
 * @internal
 */
class OrdersTableSearchQuery {
	/**
	 * Holds the Orders Table Query object.
	 *
	 * @var OrdersTableQuery
	 */
	private $query;

	/**
	 * Holds the search term to be used in the WHERE clauses.
	 *
	 * @var string
	 */
	private $search_term;

	/**
	 * Creates the JOIN and WHERE clauses needed to execute a search of orders.
	 *
	 * @internal
	 *
	 * @param OrdersTableQuery $query The order query object.
	 */
	public function __construct( OrdersTableQuery $query ) {
		$this->query       = $query;
		$this->search_term = "'" . esc_sql( '%' . urldecode( $query->get( 's' ) ) . '%' ) . "'";
	}

	/**
	 * Supplies an array of clauses to be used in an order query.
	 *
	 * @internal
	 * @throws Exception If unable to generate either the JOIN or WHERE SQL fragments.
	 *
	 * @return array {
	 *     @type string $join  JOIN clause.
	 *     @type string $where WHERE clause.
	 * }
	 */
	public function get_sql_clauses(): array {
		return array(
			'join'  => array( $this->generate_join() ),
			'where' => array( $this->generate_where() ),
		);
	}

	/**
	 * Generates the necessary JOIN clauses for the order search to be performed.
	 *
	 * @throws Exception May be triggered if a table name cannot be determined.
	 *
	 * @return string
	 */
	private function generate_join(): string {
		$orders_table = $this->query->get_table_name( 'orders' );
		$meta_table   = $this->query->get_table_name( 'meta' );
		$items_table  = $this->query->get_table_name( 'items' );

		return "
			LEFT JOIN $meta_table AS search_query_meta ON search_query_meta.order_id = $orders_table.id
			LEFT JOIN $items_table AS search_query_items ON search_query_items.order_id = $orders_table.id
		";
	}

	/**
	 * Generates the necessary WHERE clauses for the order search to be performed.
	 *
	 * @throws Exception May be triggered if a table name cannot be determined.
	 *
	 * @return string
	 */
	private function generate_where(): string {
		$where             = '';
		$meta_fields       = $this->get_meta_fields_to_be_searched();
		$possible_order_id = (string) absint( $this->query->get( 's' ) );

		// Support the passing of an order ID as the search term.
		if ( (string) $this->query->get( 's' ) === $possible_order_id ) {
			$where = $this->query->get_table_name( 'orders' ) . '.id = ' . $possible_order_id . ' OR ';
		}

		$where .= "
			(
				search_query_meta.meta_key IN ( $meta_fields )
				AND search_query_meta.meta_value LIKE $this->search_term
			)
			OR search_query_items.order_item_name LIKE $this->search_term
		";

		return " ( $where ) ";
	}

	/**
	 * Returns the order meta field keys to be searched.
	 *
	 * These will be returned as a single string, where the meta keys have been escaped, quoted and are
	 * comma-separated (ie, "'abc', 'foo'" - ready for inclusion in a SQL IN() clause).
	 *
	 * @return string
	 */
	private function get_meta_fields_to_be_searched(): string {
		/**
		 * Controls the order meta keys to be included in search queries.
		 *
		 * This hook is used when Custom Order Tables are in use: the corresponding hook when CPT-orders are in use
		 * is 'woocommerce_shop_order_search_fields'.
		 *
		 * @since 7.0.0
		 *
		 * @param array
		 */
		$meta_keys = apply_filters(
			'woocommerce_order_table_search_query_meta_keys',
			array(
				'_billing_address_index',
				'_shipping_address_index',
			)
		);

		$meta_keys = (array) array_map(
			function ( string $meta_key ): string {
				return "'" . esc_sql( wc_clean( $meta_key ) ) . "'";
			},
			$meta_keys
		);

		return implode( ',', $meta_keys );
	}
}
Internal/DataStores/Orders/OrdersTableQuery.php000064400000102127152333011040015567 0ustar00<?php
// phpcs:disable Generic.Commenting.Todo.TaskFound
/**
 * OrdersTableQuery class file.
 */

namespace Automattic\WooCommerce\Internal\DataStores\Orders;

use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;

defined( 'ABSPATH' ) || exit;

/**
 * This class provides a `WP_Query`-like interface to custom order tables.
 *
 * @property-read int   $found_orders  Number of found orders.
 * @property-read int   $found_posts   Alias of the `$found_orders` property.
 * @property-read int   $max_num_pages Max number of pages matching the current query.
 * @property-read array $orders        Order objects, or order IDs.
 * @property-read array $posts         Alias of the $orders property.
 */
class OrdersTableQuery {

	/**
	 * Values to ignore when parsing query arguments.
	 */
	public const SKIPPED_VALUES = array( '', array(), null );

	/**
	 * Regex used to catch "shorthand" comparisons in date-related query args.
	 */
	public const REGEX_SHORTHAND_DATES = '/([^.<>]*)(>=|<=|>|<|\.\.\.)([^.<>]+)/';

	/**
	 * Highest possible unsigned bigint value (unsigned bigints being the type of the `id` column).
	 *
	 * This is deliberately held as a string, rather than a numeric type, for inclusion within queries.
	 */
	private const MYSQL_MAX_UNSIGNED_BIGINT = '18446744073709551615';

	/**
	 * Names of all COT tables (orders, addresses, operational_data, meta) in the form 'table_id' => 'table name'.
	 *
	 * @var array
	 */
	private $tables = array();

	/**
	 * Column mappings for all COT tables.
	 *
	 * @var array
	 */
	private $mappings = array();

	/**
	 * Query vars after processing and sanitization.
	 *
	 * @var array
	 */
	private $args = array();

	/**
	 * Columns to be selected in the SELECT clause.
	 *
	 * @var array
	 */
	private $fields = array();

	/**
	 * Array of table aliases and conditions used to compute the JOIN clause of the query.
	 *
	 * @var array
	 */
	private $join = array();

	/**
	 * Array of fields and conditions used to compute the WHERE clause of the query.
	 *
	 * @var array
	 */
	private $where = array();

	/**
	 * Field to be used in the GROUP BY clause of the query.
	 *
	 * @var array
	 */
	private $groupby = array();

	/**
	 * Array of fields used to compute the ORDER BY clause of the query.
	 *
	 * @var array
	 */
	private $orderby = array();

	/**
	 * Limits used to compute the LIMIT clause of the query.
	 *
	 * @var array
	 */
	private $limits = array();

	/**
	 * Results (order IDs) for the current query.
	 *
	 * @var array
	 */
	private $results = array();

	/**
	 * Final SQL query to run after processing of args.
	 *
	 * @var string
	 */
	private $sql = '';

	/**
	 * The number of pages (when pagination is enabled).
	 *
	 * @var int
	 */
	private $max_num_pages = 0;

	/**
	 * The number of orders found.
	 *
	 * @var int
	 */
	private $found_orders = 0;

	/**
	 * Field query parser.
	 *
	 * @var OrdersTableFieldQuery
	 */
	private $field_query = null;

	/**
	 * Meta query parser.
	 *
	 * @var OrdersTableMetaQuery
	 */
	private $meta_query = null;

	/**
	 * Search query parser.
	 *
	 * @var OrdersTableSearchQuery?
	 */
	private $search_query = null;

	/**
	 * Date query parser.
	 *
	 * @var WP_Date_Query
	 */
	private $date_query = null;


	/**
	 * Sets up and runs the query after processing arguments.
	 *
	 * @param array $args Array of query vars.
	 */
	public function __construct( $args = array() ) {
		global $wpdb;

		$datastore = wc_get_container()->get( OrdersTableDataStore::class );

		// TODO: maybe OrdersTableDataStore::get_all_table_names() could return these keys/indices instead.
		$this->tables   = array(
			'orders'           => $datastore::get_orders_table_name(),
			'addresses'        => $datastore::get_addresses_table_name(),
			'operational_data' => $datastore::get_operational_data_table_name(),
			'meta'             => $datastore::get_meta_table_name(),
			'items'            => $wpdb->prefix . 'woocommerce_order_items',
		);
		$this->mappings = $datastore->get_all_order_column_mappings();

		$this->args = $args;

		// TODO: args to be implemented.
		unset( $this->args['customer_note'], $this->args['name'] );

		$this->build_query();
		$this->run_query();
	}

	/**
	 * Remaps some legacy and `WP_Query` specific query vars to vars available in the customer order table scheme.
	 *
	 * @return void
	 */
	private function maybe_remap_args(): void {
		$mapping = array(
			// WP_Query legacy.
			'post_date'           => 'date_created_gmt',
			'post_date_gmt'       => 'date_created_gmt',
			'post_modified'       => 'date_modified_gmt',
			'post_modified_gmt'   => 'date_updated_gmt',
			'post_status'         => 'status',
			'_date_completed'     => 'date_completed_gmt',
			'_date_paid'          => 'date_paid_gmt',
			'paged'               => 'page',
			'post_parent'         => 'parent_order_id',
			'post_parent__in'     => 'parent_order_id',
			'post_parent__not_in' => 'parent_exclude',
			'post__not_in'        => 'exclude',
			'posts_per_page'      => 'limit',
			'p'                   => 'id',
			'post__in'            => 'id',
			'post_type'           => 'type',
			'fields'              => 'return',

			'customer_user'       => 'customer_id',
			'order_currency'      => 'currency',
			'order_version'       => 'woocommerce_version',
			'cart_discount'       => 'discount_total_amount',
			'cart_discount_tax'   => 'discount_tax_amount',
			'order_shipping'      => 'shipping_total_amount',
			'order_shipping_tax'  => 'shipping_tax_amount',
			'order_tax'           => 'tax_amount',

			// Translate from WC_Order_Query to table structure.
			'version'             => 'woocommerce_version',
			'date_created'        => 'date_created_gmt',
			'date_modified'       => 'date_updated_gmt',
			'date_modified_gmt'   => 'date_updated_gmt',
			'date_completed'      => 'date_completed_gmt',
			'date_completed_gmt'  => 'date_completed_gmt',
			'date_paid'           => 'date_paid_gmt',
			'discount_total'      => 'discount_total_amount',
			'discount_tax'        => 'discount_tax_amount',
			'shipping_total'      => 'shipping_total_amount',
			'shipping_tax'        => 'shipping_tax_amount',
			'cart_tax'            => 'tax_amount',
			'total'               => 'total_amount',
			'customer_ip_address' => 'ip_address',
			'customer_user_agent' => 'user_agent',
			'parent'              => 'parent_order_id',
		);

		foreach ( $mapping as $query_key => $table_field ) {
			if ( isset( $this->args[ $query_key ] ) ) {
				$this->args[ $table_field ] = $this->args[ $query_key ];
				unset( $this->args[ $query_key ] );
			}
		}

		// meta_query.
		$this->args['meta_query'] = ( $this->arg_isset( 'meta_query' ) && is_array( $this->args['meta_query'] ) ) ? $this->args['meta_query'] : array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query

		$shortcut_meta_query = array();
		foreach ( array( 'key', 'value', 'compare', 'type', 'compare_key', 'type_key' ) as $key ) {
			if ( $this->arg_isset( "meta_{$key}" ) ) {
				$shortcut_meta_query[ $key ] = $this->args[ "meta_{$key}" ];
			}
		}

		if ( ! empty( $shortcut_meta_query ) ) {
			if ( ! empty( $this->args['meta_query'] ) ) {
				$this->args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
					'relation' => 'AND',
					$shortcut_meta_query,
					$this->args['meta_query'],
				);
			} else {
				$this->args['meta_query'] = array( $shortcut_meta_query ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
			}
		}
	}

	/**
	 * Generates a `WP_Date_Query` compatible query from a given date.
	 * YYYY-MM-DD queries have 'day' precision for backwards compatibility.
	 *
	 * @param mixed $date The date. Can be a {@see \WC_DateTime}, a timestamp or a string.
	 * @return array An array with keys 'year', 'month', 'day' and possibly 'hour', 'minute' and 'second'.
	 */
	private function date_to_date_query_arg( $date ): array {
		$result    = array(
			'year'  => '',
			'month' => '',
			'day'   => '',
		);
		$precision = 'second';

		if ( is_numeric( $date ) ) {
			$date = new \WC_DateTime( "@{$date}", new \DateTimeZone( 'UTC' ) );
		} elseif ( ! is_a( $date, 'WC_DateTime' ) ) {
			// YYYY-MM-DD queries have 'day' precision for backwards compat.
			$date      = wc_string_to_datetime( $date );
			$precision = 'day';
		}

		$result['year']  = $date->date( 'Y' );
		$result['month'] = $date->date( 'm' );
		$result['day']   = $date->date( 'd' );

		if ( 'second' === $precision ) {
			$result['hour']   = $date->date( 'H' );
			$result['minute'] = $date->date( 'i' );
			$result['second'] = $date->date( 's' );
		}

		return $result;
	}

	/**
	 * Processes date-related query args and merges the result into 'date_query'.
	 *
	 * @return void
	 * @throws \Exception When date args are invalid.
	 */
	private function process_date_args(): void {
		$valid_operators = array( '>', '>=', '=', '<=', '<', '...' );
		$date_queries    = array();
		$gmt_date_keys   = array(
			'date_created_gmt',
			'date_updated_gmt',
			'date_paid_gmt',
			'date_completed_gmt',
		);

		foreach ( array_filter( $gmt_date_keys, array( $this, 'arg_isset' ) ) as $date_key ) {
			$date_value = $this->args[ $date_key ];
			$operator   = '=';
			$dates      = array();

			if ( is_string( $date_value ) && preg_match( self::REGEX_SHORTHAND_DATES, $date_value, $matches ) ) {
				$operator = in_array( $matches[2], $valid_operators, true ) ? $matches[2] : '';

				if ( ! empty( $matches[1] ) ) {
					$dates[] = $this->date_to_date_query_arg( $matches[1] );
				}

				$dates[] = $this->date_to_date_query_arg( $matches[3] );
			} else {
				$dates[] = $this->date_to_date_query_arg( $date_value );
			}

			if ( empty( $dates ) || ! $operator || ( '...' === $operator && count( $dates ) < 2 ) ) {
				throw new \Exception( 'Invalid date_query' );
			}

			$operator_to_keys = array();

			if ( in_array( $operator, array( '>', '>=', '...' ), true ) ) {
				$operator_to_keys[] = 'after';
			}

			if ( in_array( $operator, array( '<', '<=', '...' ), true ) ) {
				$operator_to_keys[] = 'before';
			}

			$date_queries[] = array_merge(
				array(
					'column'    => $date_key,
					'inclusive' => ! in_array( $operator, array( '<', '>' ), true ),
				),
				'=' === $operator
					? end( $dates )
					: array_combine( $operator_to_keys, $dates )
			);
		}

		// Add top-level date parameters to the date_query.
		$tl_query = array();
		foreach ( array( 'hour', 'minute', 'second', 'year', 'monthnum', 'week', 'day', 'year' ) as $tl_key ) {
			if ( $this->arg_isset( $tl_key ) ) {
				$tl_query[ $tl_key ] = $this->args[ $tl_key ];
				unset( $this->args[ $tl_key ] );
			}
		}

		if ( $tl_query ) {
			$tl_query['column'] = 'date_created_gmt';
			$date_queries[]     = $tl_query;
		}

		if ( $date_queries ) {
			if ( ! $this->arg_isset( 'date_query' ) ) {
				$this->args['date_query'] = array();
			}

			$this->args['date_query'] = array_merge(
				array( 'relation' => 'AND' ),
				$date_queries,
				$this->args['date_query']
			);
		}

		$this->process_date_query_columns();
	}

	/**
	 * Makes sure all 'date_query' columns are correctly prefixed and their respective tables are being JOIN'ed.
	 *
	 * @return void
	 */
	private function process_date_query_columns() {
		global $wpdb;

		$legacy_columns = array(
			'post_date'         => 'date_created_gmt',
			'post_date_gmt'     => 'date_created_gmt',
			'post_modified'     => 'date_modified_gmt',
			'post_modified_gmt' => 'date_updated_gmt',
		);
		$table_mapping  = array(
			'date_created_gmt'   => $this->tables['orders'],
			'date_updated_gmt'   => $this->tables['orders'],
			'date_paid_gmt'      => $this->tables['operational_data'],
			'date_completed_gmt' => $this->tables['operational_data'],
		);

		if ( empty( $this->args['date_query'] ) ) {
			return;
		}

		array_walk_recursive(
			$this->args['date_query'],
			function( &$value, $key ) use ( $legacy_columns, $table_mapping, $wpdb ) {
				if ( 'column' !== $key ) {
					return;
				}

				// Translate legacy columns from wp_posts if necessary.
				$value =
					( isset( $legacy_columns[ $value ] ) || isset( $legacy_columns[ "{$wpdb->posts}.{$value}" ] ) )
					? $legacy_columns[ $value ]
					: $value;

				$table = $table_mapping[ $value ] ?? null;

				if ( ! $table ) {
					return;
				}

				$value = "{$table}.{$value}";

				if ( $table !== $this->tables['orders'] ) {
					$this->join( $table, '', '', 'inner', true );
				}
			}
		);
	}

	/**
	 * Sanitizes the 'status' query var.
	 *
	 * @return void
	 */
	private function sanitize_status(): void {
		// Sanitize status.
		$valid_statuses = array_keys( wc_get_order_statuses() );

		if ( empty( $this->args['status'] ) || 'any' === $this->args['status'] ) {
			$this->args['status'] = $valid_statuses;
		} elseif ( 'all' === $this->args['status'] ) {
			$this->args['status'] = array();
		} else {
			$this->args['status'] = is_array( $this->args['status'] ) ? $this->args['status'] : array( $this->args['status'] );

			foreach ( $this->args['status'] as &$status ) {
				$status = in_array( 'wc-' . $status, $valid_statuses, true ) ? 'wc-' . $status : $status;
			}

			$this->args['status'] = array_unique( array_filter( $this->args['status'] ) );
		}
	}

	/**
	 * Parses and sanitizes the 'orderby' query var.
	 *
	 * @return void
	 */
	private function sanitize_order_orderby(): void {
		// Allowed keys.
		// TODO: rand, meta keys, etc.
		$allowed_keys = array( 'ID', 'id', 'type', 'date', 'modified', 'parent' );

		// Translate $orderby to a valid field.
		$mapping = array(
			'ID'            => "{$this->tables['orders']}.id",
			'id'            => "{$this->tables['orders']}.id",
			'type'          => "{$this->tables['orders']}.type",
			'date'          => "{$this->tables['orders']}.date_created_gmt",
			'date_created'  => "{$this->tables['orders']}.date_created_gmt",
			'modified'      => "{$this->tables['orders']}.date_updated_gmt",
			'date_modified' => "{$this->tables['orders']}.date_updated_gmt",
			'parent'        => "{$this->tables['orders']}.parent_order_id",
			'total'         => "{$this->tables['orders']}.total_amount",
			'order_total'   => "{$this->tables['orders']}.total_amount",
		);

		$order   = $this->args['order'] ?? '';
		$orderby = $this->args['orderby'] ?? '';

		if ( 'none' === $orderby ) {
			return;
		}

		if ( is_string( $orderby ) ) {
			$orderby = array( $orderby => $order );
		}

		$this->args['orderby'] = array();
		foreach ( $orderby as $order_key => $order ) {
			if ( isset( $mapping[ $order_key ] ) ) {
				$this->args['orderby'][ $mapping[ $order_key ] ] = $this->sanitize_order( $order );
			}
		}
	}

	/**
	 * Makes sure the order in an ORDER BY statement is either 'ASC' o 'DESC'.
	 *
	 * @param string $order The unsanitized order.
	 * @return string The sanitized order.
	 */
	private function sanitize_order( string $order ): string {
		$order = strtoupper( $order );

		return in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'DESC';
	}

	/**
	 * Builds the final SQL query to be run.
	 *
	 * @return void
	 */
	private function build_query(): void {
		$this->maybe_remap_args();

		// Field queries.
		if ( ! empty( $this->args['field_query'] ) ) {
			$this->field_query = new OrdersTableFieldQuery( $this );
			$sql               = $this->field_query->get_sql_clauses();
			$this->join        = $sql['join'] ? array_merge( $this->join, $sql['join'] ) : $this->join;
			$this->where       = $sql['where'] ? array_merge( $this->where, $sql['where'] ) : $this->where;
		}

		// Build query.
		$this->process_date_args();
		$this->process_orders_table_query_args();
		$this->process_operational_data_table_query_args();
		$this->process_addresses_table_query_args();

		// Search queries.
		if ( ! empty( $this->args['s'] ) ) {
			$this->search_query = new OrdersTableSearchQuery( $this );
			$sql                = $this->search_query->get_sql_clauses();
			$this->join         = $sql['join'] ? array_merge( $this->join, $sql['join'] ) : $this->join;
			$this->where        = $sql['where'] ? array_merge( $this->where, $sql['where'] ) : $this->where;
		}

		// Meta queries.
		if ( ! empty( $this->args['meta_query'] ) ) {
			$this->meta_query = new OrdersTableMetaQuery( $this );

			$sql = $this->meta_query->get_sql_clauses();

			$this->join  = $sql['join'] ? array_merge( $this->join, $sql['join'] ) : $this->join;
			$this->where = $sql['where'] ? array_merge( $this->where, array( $sql['where'] ) ) : $this->where;

			if ( $sql['join'] ) {
				$this->groupby[] = "{$this->tables['orders']}.id";
			}
		}

		// Date queries.
		if ( ! empty( $this->args['date_query'] ) ) {
			$this->date_query = new \WP_Date_Query( $this->args['date_query'], "{$this->tables['orders']}.date_created_gmt" );
			$this->where[]    = substr( trim( $this->date_query->get_sql() ), 3 ); // WP_Date_Query includes "AND".
		}

		$this->process_orderby();
		$this->process_limit();

		$orders_table = $this->tables['orders'];

		// SELECT [fields].
		$this->fields = "{$orders_table}.id";
		$fields       = $this->fields;

		// SQL_CALC_FOUND_ROWS.
		if ( ( ! $this->arg_isset( 'no_found_rows' ) || ! $this->args['no_found_rows'] ) && $this->limits ) {
			$found_rows = 'SQL_CALC_FOUND_ROWS';
		} else {
			$found_rows = '';
		}

		// JOIN.
		$join = implode( ' ', array_unique( array_filter( array_map( 'trim', $this->join ) ) ) );

		// WHERE.
		$where = '1=1';
		foreach ( $this->where as $_where ) {
			$where .= " AND ({$_where})";
		}

		// ORDER BY.
		$orderby = $this->orderby ? ( 'ORDER BY ' . implode( ', ', $this->orderby ) ) : '';

		// LIMITS.
		$limits = '';

		if ( ! empty( $this->limits ) && count( $this->limits ) === 2 ) {
			list( $offset, $row_count ) = $this->limits;
			$row_count                  = -1 === $row_count ? self::MYSQL_MAX_UNSIGNED_BIGINT : (int) $row_count;
			$limits                     = 'LIMIT ' . (int) $offset . ', ' . $row_count;
		}

		// GROUP BY.
		$groupby = $this->groupby ? 'GROUP BY ' . implode( ', ', (array) $this->groupby ) : '';

		$this->sql = "SELECT $found_rows DISTINCT $fields FROM $orders_table $join WHERE $where $groupby $orderby $limits";
	}

	/**
	 * Returns the table alias for a given table mapping.
	 *
	 * @param string $mapping_id The mapping name (e.g. 'orders' or 'operational_data').
	 * @return string Table alias.
	 *
	 * @since 7.0.0
	 */
	public function get_core_mapping_alias( string $mapping_id ): string {
		return in_array( $mapping_id, array( 'billing_address', 'shipping_address' ), true )
			? $mapping_id
			: $this->tables[ $mapping_id ];
	}

	/**
	 * Returns an SQL JOIN clause that can be used to join the main orders table with another order table.
	 *
	 * @param string $mapping_id The mapping name (e.g. 'orders' or 'operational_data').
	 * @return string The JOIN clause.
	 *
	 * @since 7.0.0
	 */
	public function get_core_mapping_join( string $mapping_id ): string {
		global $wpdb;

		if ( 'orders' === $mapping_id ) {
			return '';
		}

		$is_address_mapping = in_array( $mapping_id, array( 'billing_address', 'shipping_address' ), true );

		$alias   = $this->get_core_mapping_alias( $mapping_id );
		$table   = $is_address_mapping ? $this->tables['addresses'] : $this->tables[ $mapping_id ];
		$join    = '';
		$join_on = '';

		$join .= "INNER JOIN `{$table}`" . ( $alias !== $table ? " AS `{$alias}`" : '' );

		if ( isset( $this->mappings[ $mapping_id ]['order_id'] ) ) {
			$join_on .= "`{$this->tables['orders']}`.id = `{$alias}`.order_id";
		}

		if ( $is_address_mapping ) {
			$join_on .= $wpdb->prepare( " AND `{$alias}`.address_type = %s", substr( $mapping_id, 0, -8 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		}

		return $join . ( $join_on ? " ON ( {$join_on} )" : '' );
	}

	/**
	 * JOINs the main orders table with another table.
	 *
	 * @param string  $table      Table name (including prefix).
	 * @param string  $alias      Table alias to use. Defaults to $table.
	 * @param string  $on         ON clause. Defaults to "wc_orders.id = {$alias}.order_id".
	 * @param string  $join_type  JOIN type: LEFT, RIGHT or INNER.
	 * @param boolean $alias_once If TRUE, table won't be JOIN'ed again if already JOIN'ed.
	 * @return void
	 * @throws \Exception When an error occurs, such as trying to re-use an alias with $alias_once = FALSE.
	 */
	private function join( string $table, string $alias = '', string $on = '', string $join_type = 'inner', bool $alias_once = false ) {
		$alias     = empty( $alias ) ? $table : $alias;
		$join_type = strtoupper( trim( $join_type ) );

		if ( $this->tables['orders'] === $alias ) {
			// translators: %s is a table name.
			throw new \Exception( sprintf( __( '%s can not be used as a table alias in OrdersTableQuery', 'woocommerce' ), $alias ) );
		}

		if ( empty( $on ) ) {
			if ( $this->tables['orders'] === $table ) {
				$on = "{$this->tables['orders']}.id = {$alias}.id";
			} else {
				$on = "{$this->tables['orders']}.id = {$alias}.order_id";
			}
		}

		if ( isset( $this->join[ $alias ] ) ) {
			if ( ! $alias_once ) {
				// translators: %s is a table name.
				throw new \Exception( sprintf( __( 'Can not re-use table alias "%s" in OrdersTableQuery.', 'woocommerce' ), $alias ) );
			}

			return;
		}

		if ( '' === $join_type || ! in_array( $join_type, array( 'LEFT', 'RIGHT', 'INNER' ), true ) ) {
			$join_type = 'INNER';
		}

		$sql_join  = '';
		$sql_join .= "{$join_type} JOIN {$table} ";
		$sql_join .= ( $alias !== $table ) ? "AS {$alias} " : '';
		$sql_join .= "ON ( {$on} )";

		$this->join[ $alias ] = $sql_join;
	}

	/**
	 * Generates a properly escaped and sanitized WHERE condition for a given field.
	 *
	 * @param string $table    The table the field belongs to.
	 * @param string $field    The field or column name.
	 * @param string $operator The operator to use in the condition. Defaults to '=' or 'IN' depending on $value.
	 * @param mixed  $value    The value.
	 * @param string $type     The column type as specified in {@see OrdersTableDataStore} column mappings.
	 * @return string The resulting WHERE condition.
	 */
	public function where( string $table, string $field, string $operator, $value, string $type ): string {
		global $wpdb;

		$db_util  = wc_get_container()->get( DatabaseUtil::class );
		$operator = strtoupper( '' !== $operator ? $operator : '=' );

		try {
			$format = $db_util->get_wpdb_format_for_type( $type );
		} catch ( \Exception $e ) {
			$format = '%s';
		}

		// = and != can be shorthands for IN and NOT in for array values.
		if ( is_array( $value ) && '=' === $operator ) {
			$operator = 'IN';
		} elseif ( is_array( $value ) && '!=' === $operator ) {
			$operator = 'NOT IN';
		}

		if ( ! in_array( $operator, array( '=', '!=', 'IN', 'NOT IN' ), true ) ) {
			return false;
		}

		if ( is_array( $value ) ) {
			$value = array_map( array( $db_util, 'format_object_value_for_db' ), $value, array_fill( 0, count( $value ), $type ) );
		} else {
			$value = $db_util->format_object_value_for_db( $value, $type );
		}

		if ( is_array( $value ) ) {
			$placeholder = array_fill( 0, count( $value ), $format );
			$placeholder = '(' . implode( ',', $placeholder ) . ')';
		} else {
			$placeholder = $format;
		}

		$sql = $wpdb->prepare( "{$table}.{$field} {$operator} {$placeholder}", $value ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare

		return $sql;
	}

	/**
	 * Processes fields related to the orders table.
	 *
	 * @return void
	 */
	private function process_orders_table_query_args(): void {
		$this->sanitize_status();

		$fields = array_filter(
			array(
				'id',
				'status',
				'type',
				'currency',
				'tax_amount',
				'customer_id',
				'billing_email',
				'total_amount',
				'parent_order_id',
				'payment_method',
				'payment_method_title',
				'transaction_id',
				'ip_address',
				'user_agent',
			),
			array( $this, 'arg_isset' )
		);

		foreach ( $fields as $arg_key ) {
			$this->where[] = $this->where( $this->tables['orders'], $arg_key, '=', $this->args[ $arg_key ], $this->mappings['orders'][ $arg_key ]['type'] );
		}

		if ( $this->arg_isset( 'parent_exclude' ) ) {
			$this->where[] = $this->where( $this->tables['orders'], 'parent_order_id', '!=', $this->args['parent_exclude'], 'int' );
		}

		if ( $this->arg_isset( 'exclude' ) ) {
			$this->where[] = $this->where( $this->tables['orders'], 'id', '!=', $this->args['exclude'], 'int' );
		}

		// 'customer' is a very special field.
		if ( $this->arg_isset( 'customer' ) ) {
			$customer_query = $this->generate_customer_query( $this->args['customer'] );

			if ( $customer_query ) {
				$this->where[] = $customer_query;
			}
		}
	}

	/**
	 * Generate SQL conditions for the 'customer' query.
	 *
	 * @param array  $values   List of customer ids or emails.
	 * @param string $relation 'OR' or 'AND' relation used to build the customer query.
	 * @return string SQL to be used in a WHERE clause.
	 */
	private function generate_customer_query( $values, string $relation = 'OR' ): string {
		$values = is_array( $values ) ? $values : array( $values );
		$ids    = array();
		$emails = array();

		foreach ( $values as $value ) {
			if ( is_array( $value ) ) {
				$sql      = $this->generate_customer_query( $value, 'AND' );
				$pieces[] = $sql ? '(' . $sql . ')' : '';
			} elseif ( is_numeric( $value ) ) {
				$ids[] = absint( $value );
			} elseif ( is_string( $value ) && is_email( $value ) ) {
				$emails[] = sanitize_email( $value );
			}
		}

		if ( $ids ) {
			$pieces[] = $this->where( $this->tables['orders'], 'customer_id', '=', $ids, 'int' );
		}

		if ( $emails ) {
			$pieces[] = $this->where( $this->tables['orders'], 'billing_email', '=', $emails, 'string' );
		}

		return $pieces ? implode( " $relation ", $pieces ) : '';
	}

	/**
	 * Processes fields related to the operational data table.
	 *
	 * @return void
	 */
	private function process_operational_data_table_query_args(): void {
		$fields = array_filter(
			array(
				'created_via',
				'woocommerce_version',
				'prices_include_tax',
				'order_key',
				'discount_total_amount',
				'discount_tax_amount',
				'shipping_total_amount',
				'shipping_tax_amount',
			),
			array( $this, 'arg_isset' )
		);

		if ( ! $fields ) {
			return;
		}

		$this->join(
			$this->tables['operational_data'],
			'',
			'',
			'inner',
			true
		);

		foreach ( $fields as $arg_key ) {
			$this->where[] = $this->where( $this->tables['operational_data'], $arg_key, '=', $this->args[ $arg_key ], $this->mappings['operational_data'][ $arg_key ]['type'] );
		}
	}

	/**
	 * Processes fields related to the addresses table.
	 *
	 * @return void
	 */
	private function process_addresses_table_query_args(): void {
		global $wpdb;

		foreach ( array( 'billing', 'shipping' ) as $address_type ) {
			$fields = array_filter(
				array(
					$address_type . '_first_name',
					$address_type . '_last_name',
					$address_type . '_company',
					$address_type . '_address_1',
					$address_type . '_address_2',
					$address_type . '_city',
					$address_type . '_state',
					$address_type . '_postcode',
					$address_type . '_country',
					$address_type . '_phone',
				),
				array( $this, 'arg_isset' )
			);

			if ( ! $fields ) {
				continue;
			}

			$this->join(
				$this->tables['addresses'],
				$address_type,
				$wpdb->prepare( "{$this->tables['orders']}.id = {$address_type}.order_id AND {$address_type}.address_type = %s", $address_type ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				'inner',
				false
			);

			foreach ( $fields as $arg_key ) {
				$column_name = str_replace( "{$address_type}_", '', $arg_key );

				$this->where[] = $this->where(
					$address_type,
					$column_name,
					'=',
					$this->args[ $arg_key ],
					$this->mappings[ "{$address_type}_address" ][ $column_name ]['type']
				);
			}
		}
	}

	/**
	 * Generates the ORDER BY clause.
	 *
	 * @return void
	 */
	private function process_orderby(): void {
		// 'order' and 'orderby' vars.
		$this->args['order'] = $this->sanitize_order( $this->args['order'] ?? '' );
		$this->sanitize_order_orderby();

		$orderby = $this->args['orderby'];

		if ( 'none' === $orderby ) {
			$this->orderby = '';
			return;
		}

		$orderby_array = array();
		foreach ( $this->args['orderby'] as $_orderby => $order ) {
			$orderby_array[] = "{$_orderby} {$order}";
		}

		$this->orderby = $orderby_array;
	}

	/**
	 * Generates the limits to be used in the LIMIT clause.
	 *
	 * @return void
	 */
	private function process_limit(): void {
		$row_count = ( $this->arg_isset( 'limit' ) ? (int) $this->args['limit'] : false );
		$page      = ( $this->arg_isset( 'page' ) ? absint( $this->args['page'] ) : 1 );
		$offset    = ( $this->arg_isset( 'offset' ) ? absint( $this->args['offset'] ) : false );

		// Bool false indicates no limit was specified; less than -1 means an invalid value was passed (such as -3).
		if ( false === $row_count || $row_count < -1 ) {
			return;
		}

		if ( false === $offset && $row_count > -1 ) {
			$offset = (int) ( ( $page - 1 ) * $row_count );
		}

		$this->limits = array( $offset, $row_count );
	}

	/**
	 * Checks if a query var is set (i.e. not one of the "skipped values").
	 *
	 * @param string $arg_key Query var.
	 * @return bool TRUE if query var is set.
	 */
	public function arg_isset( string $arg_key ): bool {
		return ( isset( $this->args[ $arg_key ] ) && ! in_array( $this->args[ $arg_key ], self::SKIPPED_VALUES, true ) );
	}

	/**
	 * Runs the SQL query.
	 *
	 * @return void
	 */
	private function run_query(): void {
		global $wpdb;

		// Run query.
		$this->orders = array_map( 'absint', $wpdb->get_col( $this->sql ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		// Set max_num_pages and found_orders if necessary.
		if ( ( $this->arg_isset( 'no_found_rows' ) && ! $this->args['no_found_rows'] ) || empty( $this->orders ) ) {
			return;
		}

		if ( $this->limits ) {
			$this->found_orders  = absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) );
			$this->max_num_pages = (int) ceil( $this->found_orders / $this->args['limit'] );
		} else {
			$this->found_orders = count( $this->orders );
		}
	}

	/**
	 * Make some private available for backwards compatibility.
	 *
	 * @param string $name Property to get.
	 * @return mixed
	 */
	public function __get( string $name ) {
		switch ( $name ) {
			case 'found_orders':
			case 'found_posts':
				return $this->found_orders;
			case 'max_num_pages':
				return $this->max_num_pages;
			case 'posts':
			case 'orders':
				return $this->results;
			case 'request':
				return $this->sql;
			default:
				break;
		}
	}

	/**
	 * Returns the value of one of the query arguments.
	 *
	 * @param string $arg_name Query var.
	 * @return mixed
	 */
	public function get( string $arg_name ) {
		return $this->args[ $arg_name ] ?? null;
	}

	/**
	 * Returns the name of one of the OrdersTableDatastore tables.
	 *
	 * @param string $table_id Table identifier. One of 'orders', 'operational_data', 'addresses', 'meta'.
	 * @return string The prefixed table name.
	 * @throws \Exception When table ID is not found.
	 */
	public function get_table_name( string $table_id = '' ): string {
		if ( ! isset( $this->tables[ $table_id ] ) ) {
			// Translators: %s is a table identifier.
			throw new \Exception( sprintf( __( 'Invalid table id: %s.', 'woocommerce' ), $table_id ) );
		}

		return $this->tables[ $table_id ];
	}

	/**
	 * Finds table and mapping information about a field or column.
	 *
	 * @param string $field Field to look for in `<mapping|field_name>.<column|field_name>` format or just `<field_name>`.
	 * @return false|array {
	 *     @type string $table      Full table name where the field is located.
	 *     @type string $mapping_id Unprefixed table or mapping name.
	 *     @type string $field_name Name of the corresponding order field.
	 *     @type string $column     Column in $table that corresponds to the field.
	 *     @type string $type       Field type.
	 * }
	 */
	public function get_field_mapping_info( $field ) {
		global $wpdb;

		$result = array(
			'table'       => '',
			'mapping_id'  => '',
			'field_name'  => '',
			'column'      => '',
			'column_type' => '',
		);

		$mappings_to_search = array();

		if ( false !== strstr( $field, '.' ) ) {
			list( $mapping_or_table, $field_name_or_col ) = explode( '.', $field );

			$mapping_or_table = substr( $mapping_or_table, 0, strlen( $wpdb->prefix ) ) === $wpdb->prefix ? substr( $mapping_or_table, strlen( $wpdb->prefix ) ) : $mapping_or_table;
			$mapping_or_table = 'wc_' === substr( $mapping_or_table, 0, 3 ) ? substr( $mapping_or_table, 3 ) : $mapping_or_table;

			if ( isset( $this->mappings[ $mapping_or_table ] ) ) {
				if ( isset( $this->mappings[ $mapping_or_table ][ $field_name_or_col ] ) ) {
					$result['mapping_id'] = $mapping_or_table;
					$result['column']     = $field_name_or_col;
				} else {
					$mappings_to_search = array( $mapping_or_table );
				}
			}
		} else {
			$field_name_or_col  = $field;
			$mappings_to_search = array_keys( $this->mappings );
		}

		foreach ( $mappings_to_search as $mapping_id ) {
			foreach ( $this->mappings[ $mapping_id ] as $column_name => $column_data ) {
				if ( isset( $column_data['name'] ) && $column_data['name'] === $field_name_or_col ) {
					$result['mapping_id'] = $mapping_id;
					$result['column']     = $column_name;
					break 2;
				}
			}
		}

		if ( ! $result['mapping_id'] || ! $result['column'] ) {
			return false;
		}

		$field_info = $this->mappings[ $result['mapping_id'] ][ $result['column'] ];

		$result['field_name']  = $field_info['name'];
		$result['column_type'] = $field_info['type'];
		$result['table']       = ( in_array( $result['mapping_id'], array( 'billing_address', 'shipping_address' ), true ) )
								? $this->tables['addresses']
								: $this->tables[ $result['mapping_id'] ];

		return $result;
	}

}
Internal/DataStores/Orders/DataSynchronizer.php000064400000042126152333011040015624 0ustar00<?php
/**
 * DataSynchronizer class file.
 */

namespace Automattic\WooCommerce\Internal\DataStores\Orders;

use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessorInterface;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;

defined( 'ABSPATH' ) || exit;

/**
 * This class handles the database structure creation and the data synchronization for the custom orders tables. Its responsibilites are:
 *
 * - Providing entry points for creating and deleting the required database tables.
 * - Synchronizing changes between the custom orders tables and the posts table whenever changes in orders happen.
 */
class DataSynchronizer implements BatchProcessorInterface {

	use AccessiblePrivateMethods;

	public const ORDERS_DATA_SYNC_ENABLED_OPTION           = 'woocommerce_custom_orders_table_data_sync_enabled';
	private const INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_initial_orders_pending_sync_count';
	public const PENDING_SYNCHRONIZATION_FINISHED_ACTION   = 'woocommerce_orders_sync_finished';
	public const PLACEHOLDER_ORDER_POST_TYPE               = 'shop_order_placehold';

	private const ORDERS_SYNC_BATCH_SIZE = 250;
	// Allowed values for $type in get_ids_of_orders_pending_sync method.
	public const ID_TYPE_MISSING_IN_ORDERS_TABLE = 0;
	public const ID_TYPE_MISSING_IN_POSTS_TABLE  = 1;
	public const ID_TYPE_DIFFERENT_UPDATE_DATE   = 2;

	/**
	 * The data store object to use.
	 *
	 * @var OrdersTableDataStore
	 */
	private $data_store;

	/**
	 * The database util object to use.
	 *
	 * @var DatabaseUtil
	 */
	private $database_util;

	/**
	 * The posts to COT migrator to use.
	 *
	 * @var PostsToOrdersMigrationController
	 */
	private $posts_to_cot_migrator;

	/**
	 * Logger object to be used to log events.
	 *
	 * @var \WC_Logger
	 */
	private $error_logger;

	/**
	 * Class constructor.
	 */
	public function __construct() {
		self::add_action( 'deleted_post', array( $this, 'handle_deleted_post' ), 10, 2 );
		self::add_action( 'woocommerce_new_order', array( $this, 'handle_updated_order' ), 100 );
		self::add_action( 'woocommerce_update_order', array( $this, 'handle_updated_order' ), 100 );
		self::add_filter( 'woocommerce_feature_description_tip', array( $this, 'handle_feature_description_tip' ), 10, 3 );
	}

	/**
	 * Class initialization, invoked by the DI container.
	 *
	 * @param OrdersTableDataStore             $data_store The data store to use.
	 * @param DatabaseUtil                     $database_util The database util class to use.
	 * @param PostsToOrdersMigrationController $posts_to_cot_migrator The posts to COT migration class to use.
	 * @param LegacyProxy                      $legacy_proxy The legacy proxy instance to use.
	 * @internal
	 */
	final public function init( OrdersTableDataStore $data_store, DatabaseUtil $database_util, PostsToOrdersMigrationController $posts_to_cot_migrator, LegacyProxy $legacy_proxy ) {
		$this->data_store            = $data_store;
		$this->database_util         = $database_util;
		$this->posts_to_cot_migrator = $posts_to_cot_migrator;
		$this->error_logger          = $legacy_proxy->call_function( 'wc_get_logger' );
	}

	/**
	 * Does the custom orders tables exist in the database?
	 *
	 * @return bool True if the custom orders tables exist in the database.
	 */
	public function check_orders_table_exists(): bool {
		$missing_tables = $this->database_util->get_missing_tables( $this->data_store->get_database_schema() );

		return count( $missing_tables ) === 0;
	}

	/**
	 * Create the custom orders database tables.
	 */
	public function create_database_tables() {
		$this->database_util->dbdelta( $this->data_store->get_database_schema() );
	}

	/**
	 * Delete the custom orders database tables.
	 */
	public function delete_database_tables() {
		$table_names = $this->data_store->get_all_table_names();

		foreach ( $table_names as $table_name ) {
			$this->database_util->drop_database_table( $table_name );
		}
	}

	/**
	 * Is the data sync between old and new tables currently enabled?
	 *
	 * @return bool
	 */
	public function data_sync_is_enabled(): bool {
		return 'yes' === get_option( self::ORDERS_DATA_SYNC_ENABLED_OPTION );
	}

	/**
	 * Get the current sync process status.
	 * The information is meaningful only if pending_data_sync_is_in_progress return true.
	 *
	 * @return array
	 */
	public function get_sync_status() {
		return array(
			'initial_pending_count' => (int) get_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION, 0 ),
			'current_pending_count' => $this->get_total_pending_count(),
		);
	}

	/**
	 * Get the total number of orders pending synchronization.
	 *
	 * @return int
	 */
	public function get_current_orders_pending_sync_count_cached() : int {
		return $this->get_current_orders_pending_sync_count( true );
	}

	/**
	 * Calculate how many orders need to be synchronized currently.
	 * A database query is performed to get how many orders match one of the following:
	 *
	 * - Existing in the authoritative table but not in the backup table.
	 * - Existing in both tables, but they have a different update date.
	 *
	 * @param bool $use_cache Whether to use the cached value instead of fetching from database.
	 */
	public function get_current_orders_pending_sync_count( $use_cache = false ): int {
		global $wpdb;

		if ( $use_cache ) {
			$pending_count = wp_cache_get( 'woocommerce_hpos_pending_sync_count' );
			if ( false !== $pending_count ) {
				return (int) $pending_count;
			}
		}
		$orders_table                = $this->data_store::get_orders_table_name();
		$order_post_types            = wc_get_order_types( 'cot-migration' );
		$order_post_type_placeholder = implode( ', ', array_fill( 0, count( $order_post_types ), '%s' ) );

		if ( $this->custom_orders_table_is_authoritative() ) {
			$missing_orders_count_sql = "
SELECT COUNT(1) FROM $wpdb->posts posts
INNER JOIN $orders_table orders ON posts.id=orders.id
WHERE posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "'
 AND orders.status not in ( 'auto-draft' )
";
			$operator                 = '>';
		} else {
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholder is prepared.
			$missing_orders_count_sql = $wpdb->prepare(
				"
SELECT COUNT(1) FROM $wpdb->posts posts
LEFT JOIN $orders_table orders ON posts.id=orders.id
WHERE
  posts.post_type in ($order_post_type_placeholder)
  AND posts.post_status != 'auto-draft'
  AND orders.id IS NULL",
				$order_post_types
			);
			// phpcs:enable
			$operator = '<';
		}

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $missing_orders_count_sql is prepared.
		$sql = $wpdb->prepare(
			"
SELECT(
	($missing_orders_count_sql)
	+
	(SELECT COUNT(1) FROM (
		SELECT orders.id FROM $orders_table orders
		JOIN $wpdb->posts posts on posts.ID = orders.id
		WHERE
		  posts.post_type IN ($order_post_type_placeholder)
		  AND orders.date_updated_gmt $operator posts.post_modified_gmt
	) x)
) count",
			$order_post_types
		);
		// phpcs:enable

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$pending_count = (int) $wpdb->get_var( $sql );
		wp_cache_set( 'woocommerce_hpos_pending_sync_count', $pending_count );
		return $pending_count;
	}

	/**
	 * Is the custom orders table the authoritative data source for orders currently?
	 *
	 * @return bool Whether the custom orders table the authoritative data source for orders currently.
	 */
	public function custom_orders_table_is_authoritative(): bool {
		return wc_string_to_bool( get_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) );
	}

	/**
	 * Get a list of ids of orders than are out of sync.
	 *
	 * Valid values for $type are:
	 *
	 * ID_TYPE_MISSING_IN_ORDERS_TABLE: orders that exist in posts table but not in orders table.
	 * ID_TYPE_MISSING_IN_POSTS_TABLE: orders that exist in orders table but not in posts table (the corresponding post entries are placeholders).
	 * ID_TYPE_DIFFERENT_UPDATE_DATE: orders that exist in both tables but have different last update dates.
	 *
	 * @param int $type One of ID_TYPE_MISSING_IN_ORDERS_TABLE, ID_TYPE_MISSING_IN_POSTS_TABLE, ID_TYPE_DIFFERENT_UPDATE_DATE.
	 * @param int $limit Maximum number of ids to return.
	 * @return array An array of order ids.
	 * @throws \Exception Invalid parameter.
	 */
	public function get_ids_of_orders_pending_sync( int $type, int $limit ) {
		global $wpdb;

		if ( $limit < 1 ) {
			throw new \Exception( '$limit must be at least 1' );
		}

		$orders_table                 = $this->data_store::get_orders_table_name();
		$order_post_types             = wc_get_order_types( 'cot-migration' );
		$order_post_type_placeholders = implode( ', ', array_fill( 0, count( $order_post_types ), '%s' ) );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		switch ( $type ) {
			case self::ID_TYPE_MISSING_IN_ORDERS_TABLE:
				// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholders is prepared.
				$sql = $wpdb->prepare(
					"
SELECT posts.ID FROM $wpdb->posts posts
LEFT JOIN $orders_table orders ON posts.ID = orders.id
WHERE
  posts.post_type IN ($order_post_type_placeholders)
  AND posts.post_status != 'auto-draft'
  AND orders.id IS NULL",
					$order_post_types
				);
				break;
			case self::ID_TYPE_MISSING_IN_POSTS_TABLE:
				$sql = "
SELECT posts.ID FROM $wpdb->posts posts
INNER JOIN $orders_table orders ON posts.id=orders.id
WHERE posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "'
AND orders.status not in ( 'auto-draft' )
";
				break;
			case self::ID_TYPE_DIFFERENT_UPDATE_DATE:
				$operator = $this->custom_orders_table_is_authoritative() ? '>' : '<';
				// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholders is prepared.
				$sql = $wpdb->prepare(
					"
SELECT orders.id FROM $orders_table orders
JOIN $wpdb->posts posts on posts.ID = orders.id
WHERE
  posts.post_type IN ($order_post_type_placeholders)
  AND orders.date_updated_gmt $operator posts.post_modified_gmt
",
					$order_post_types
				);
				// phpcs:enable
				break;
			default:
				throw new \Exception( 'Invalid $type, must be one of the ID_TYPE_... constants.' );
		}
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		// phpcs:ignore WordPress.DB
		return array_map( 'intval', $wpdb->get_col( $sql . " LIMIT $limit" ) );
	}

	/**
	 * Cleanup all the synchronization status information,
	 * because the process has been disabled by the user via settings,
	 * or because there's nothing left to synchronize.
	 */
	public function cleanup_synchronization_state() {
		delete_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION );
	}

	/**
	 * Process data for current batch.
	 *
	 * @param array $batch Batch details.
	 */
	public function process_batch( array $batch ) : void {
		if ( $this->custom_orders_table_is_authoritative() ) {
			foreach ( $batch as $id ) {
				$order = wc_get_order( $id );
				if ( ! $order ) {
					$this->error_logger->error( "Order $id not found during batch process, skipping." );
					continue;
				}
				$data_store = $order->get_data_store();
				$data_store->backfill_post_record( $order );
			}
		} else {
			$this->posts_to_cot_migrator->migrate_orders( $batch );
		}
		if ( 0 === $this->get_total_pending_count() ) {
			$this->cleanup_synchronization_state();
		}
	}

	/**
	 * Get total number of pending records that require update.
	 *
	 * @return int Number of pending records.
	 */
	public function get_total_pending_count(): int {
		return $this->get_current_orders_pending_sync_count();
	}

	/**
	 * Returns the batch with records that needs to be processed for a given size.
	 *
	 * @param int $size Size of the batch.
	 *
	 * @return array Batch of records.
	 */
	public function get_next_batch_to_process( int $size ): array {
		if ( $this->custom_orders_table_is_authoritative() ) {
			$order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_MISSING_IN_POSTS_TABLE, $size );
		} else {
			$order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_MISSING_IN_ORDERS_TABLE, $size );
		}
		if ( count( $order_ids ) >= $size ) {
			return $order_ids;
		}

		$order_ids = $order_ids + $this->get_ids_of_orders_pending_sync( self::ID_TYPE_DIFFERENT_UPDATE_DATE, $size - count( $order_ids ) );
		return $order_ids;
	}

	/**
	 * Default batch size to use.
	 *
	 * @return int Default batch size.
	 */
	public function get_default_batch_size(): int {
		$batch_size = self::ORDERS_SYNC_BATCH_SIZE;

		if ( $this->custom_orders_table_is_authoritative() ) {
			// Back-filling is slower than migration.
			$batch_size = absint( self::ORDERS_SYNC_BATCH_SIZE / 10 ) + 1;
		}
		/**
		 * Filter to customize the count of orders that will be synchronized in each step of the custom orders table to/from posts table synchronization process.
		 *
		 * @since 6.6.0
		 *
		 * @param int Default value for the count.
		 */
		return apply_filters( 'woocommerce_orders_cot_and_posts_sync_step_size', $batch_size );
	}

	/**
	 * A user friendly name for this process.
	 *
	 * @return string Name of the process.
	 */
	public function get_name(): string {
		return 'Order synchronizer';
	}

	/**
	 * A user friendly description for this process.
	 *
	 * @return string Description.
	 */
	public function get_description(): string {
		return 'Synchronizes orders between posts and custom order tables.';
	}

	/**
	 * Handle the 'deleted_post' action.
	 *
	 * When posts is authoritative and sync is enabled, deleting a post also deletes COT data.
	 *
	 * @param int     $postid The post id.
	 * @param WP_Post $post The deleted post.
	 */
	private function handle_deleted_post( $postid, $post ): void {
		if ( 'shop_order' === $post->post_type && ! $this->custom_orders_table_is_authoritative() && $this->data_sync_is_enabled() ) {
			$this->data_store->delete_order_data_from_custom_order_tables( $postid );
		}
	}

	/**
	 * Handle the 'woocommerce_update_order' action.
	 *
	 * When posts is authoritative and sync is enabled, updating a post triggers a corresponding change in the COT table.
	 *
	 * @param int $order_id The order id.
	 */
	private function handle_updated_order( $order_id ): void {
		if ( ! $this->custom_orders_table_is_authoritative() && $this->data_sync_is_enabled() ) {
			$this->posts_to_cot_migrator->migrate_orders( array( $order_id ) );
		}
	}

	/**
	 * Handle the 'woocommerce_feature_description_tip' filter.
	 *
	 * When the COT feature is enabled and there are orders pending sync (in either direction),
	 * show a "you should ync before disabling" warning under the feature in the features page.
	 * Skip this if the UI prevents changing the feature enable status.
	 *
	 * @param string $desc_tip The original description tip for the feature.
	 * @param string $feature_id The feature id.
	 * @param bool   $ui_disabled True if the UI doesn't allow to enable or disable the feature.
	 * @return string The new description tip for the feature.
	 */
	private function handle_feature_description_tip( $desc_tip, $feature_id, $ui_disabled ): string {
		if ( 'custom_order_tables' !== $feature_id || $ui_disabled ) {
			return $desc_tip;
		}

		$features_controller = wc_get_container()->get( FeaturesController::class );
		$feature_is_enabled  = $features_controller->feature_is_enabled( 'custom_order_tables' );
		if ( ! $feature_is_enabled ) {
			return $desc_tip;
		}

		$pending_sync_count = $this->get_current_orders_pending_sync_count();
		if ( ! $pending_sync_count ) {
			return $desc_tip;
		}

		if ( $this->custom_orders_table_is_authoritative() ) {
			$extra_tip = sprintf(
				_n(
					"⚠ There's one order pending sync from the orders table to the posts table. The feature shouldn't be disabled until this order is synchronized.",
					"⚠ There are %1\$d orders pending sync from the orders table to the posts table. The feature shouldn't be disabled until these orders are synchronized.",
					$pending_sync_count,
					'woocommerce'
				),
				$pending_sync_count
			);
		} else {
			$extra_tip = sprintf(
				_n(
					"⚠ There's one order pending sync from the posts table to the orders table. The feature shouldn't be disabled until this order is synchronized.",
					"⚠ There are %1\$d orders pending sync from the posts table to the orders table. The feature shouldn't be disabled until these orders are synchronized.",
					$pending_sync_count,
					'woocommerce'
				),
				$pending_sync_count
			);
		}

		$cot_settings_url = add_query_arg(
			array(
				'page'    => 'wc-settings',
				'tab'     => 'advanced',
				'section' => 'custom_data_stores',
			),
			admin_url( 'admin.php' )
		);

		/* translators: %s = URL of the custom data stores settings page */
		$manage_cot_settings_link = sprintf( __( "<a href='%s'>Manage orders synchronization</a>", 'woocommerce' ), $cot_settings_url );

		return $desc_tip ? "{$desc_tip}<br/>{$extra_tip} {$manage_cot_settings_link}" : "{$extra_tip} {$manage_cot_settings_link}";
	}
}
Internal/DataStores/Orders/OrdersTableDataStore.php000064400000221235152333011040016352 0ustar00<?php
/**
 * OrdersTableDataStore class file.
 */

namespace Automattic\WooCommerce\Internal\DataStores\Orders;

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use WC_Data;
use WC_Order;

defined( 'ABSPATH' ) || exit;

/**
 * This class is the standard data store to be used when the custom orders table is in use.
 */
class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements \WC_Object_Data_Store_Interface, \WC_Order_Data_Store_Interface {

	/**
	 * Order IDs for which we are checking read on sync in the current request.
	 *
	 * @var array.
	 */
	private static $reading_order_ids = array();

	/**
	 * Data stored in meta keys, but not considered "meta" for an order.
	 *
	 * @since 7.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'_customer_user',
		'_order_key',
		'_order_currency',
		'_billing_first_name',
		'_billing_last_name',
		'_billing_company',
		'_billing_address_1',
		'_billing_address_2',
		'_billing_city',
		'_billing_state',
		'_billing_postcode',
		'_billing_country',
		'_billing_email',
		'_billing_phone',
		'_shipping_first_name',
		'_shipping_last_name',
		'_shipping_company',
		'_shipping_address_1',
		'_shipping_address_2',
		'_shipping_city',
		'_shipping_state',
		'_shipping_postcode',
		'_shipping_country',
		'_shipping_phone',
		'_completed_date',
		'_paid_date',
		'_edit_lock',
		'_edit_last',
		'_cart_discount',
		'_cart_discount_tax',
		'_order_shipping',
		'_order_shipping_tax',
		'_order_tax',
		'_order_total',
		'_payment_method',
		'_payment_method_title',
		'_transaction_id',
		'_customer_ip_address',
		'_customer_user_agent',
		'_created_via',
		'_order_version',
		'_prices_include_tax',
		'_date_completed',
		'_date_paid',
		'_payment_tokens',
		'_billing_address_index',
		'_shipping_address_index',
		'_recorded_sales',
		'_recorded_coupon_usage_counts',
		'_download_permissions_granted',
		'_order_stock_reduced',
		'_new_order_email_sent',
	);

	/**
	 * Handles custom metadata in the wc_orders_meta table.
	 *
	 * @var OrdersTableDataStoreMeta
	 */
	protected $data_store_meta;

	/**
	 * The database util object to use.
	 *
	 * @var DatabaseUtil
	 */
	protected $database_util;

	/**
	 * The posts data store object to use.
	 *
	 * @var \WC_Order_Data_Store_CPT
	 */
	private $cpt_data_store;

	/**
	 * Logger object to be used to log events.
	 *
	 * @var \WC_Logger
	 */
	private $error_logger;

	/**
	 * Initialize the object.
	 *
	 * @internal
	 * @param OrdersTableDataStoreMeta $data_store_meta Metadata instance.
	 * @param DatabaseUtil             $database_util   The database util instance to use.
	 * @param LegacyProxy              $legacy_proxy    The legacy proxy instance to use.
	 *
	 * @return void
	 */
	final public function init( OrdersTableDataStoreMeta $data_store_meta, DatabaseUtil $database_util, LegacyProxy $legacy_proxy ) {
		$this->data_store_meta    = $data_store_meta;
		$this->database_util      = $database_util;
		$this->error_logger       = $legacy_proxy->call_function( 'wc_get_logger' );
		$this->internal_meta_keys = $this->get_internal_meta_keys();
	}

	/**
	 * Get the custom orders table name.
	 *
	 * @return string The custom orders table name.
	 */
	public static function get_orders_table_name() {
		global $wpdb;

		return $wpdb->prefix . 'wc_orders';
	}

	/**
	 * Get the order addresses table name.
	 *
	 * @return string The order addresses table name.
	 */
	public static function get_addresses_table_name() {
		global $wpdb;

		return $wpdb->prefix . 'wc_order_addresses';
	}

	/**
	 * Get the orders operational data table name.
	 *
	 * @return string The orders operational data table name.
	 */
	public static function get_operational_data_table_name() {
		global $wpdb;

		return $wpdb->prefix . 'wc_order_operational_data';
	}

	/**
	 * Get the orders meta data table name.
	 *
	 * @return string Name of order meta data table.
	 */
	public static function get_meta_table_name() {
		global $wpdb;

		return $wpdb->prefix . 'wc_orders_meta';
	}

	/**
	 * Get the names of all the tables involved in the custom orders table feature.
	 *
	 * @return string[]
	 */
	public function get_all_table_names() {
		return array(
			$this->get_orders_table_name(),
			$this->get_addresses_table_name(),
			$this->get_operational_data_table_name(),
			$this->get_meta_table_name(),
		);
	}

	/**
	 * Table column to WC_Order mapping for wc_orders table.
	 *
	 * @var \string[][]
	 */
	protected $order_column_mapping = array(
		'id'                   => array(
			'type' => 'int',
			'name' => 'id',
		),
		'status'               => array(
			'type' => 'string',
			'name' => 'status',
		),
		'type'                 => array(
			'type' => 'string',
			'name' => 'type',
		),
		'currency'             => array(
			'type' => 'string',
			'name' => 'currency',
		),
		'tax_amount'           => array(
			'type' => 'decimal',
			'name' => 'cart_tax',
		),
		'total_amount'         => array(
			'type' => 'decimal',
			'name' => 'total',
		),
		'customer_id'          => array(
			'type' => 'int',
			'name' => 'customer_id',
		),
		'billing_email'        => array(
			'type' => 'string',
			'name' => 'billing_email',
		),
		'date_created_gmt'     => array(
			'type' => 'date',
			'name' => 'date_created',
		),
		'date_updated_gmt'     => array(
			'type' => 'date',
			'name' => 'date_modified',
		),
		'parent_order_id'      => array(
			'type' => 'int',
			'name' => 'parent_id',
		),
		'payment_method'       => array(
			'type' => 'string',
			'name' => 'payment_method',
		),
		'payment_method_title' => array(
			'type' => 'string',
			'name' => 'payment_method_title',
		),
		'ip_address'           => array(
			'type' => 'string',
			'name' => 'customer_ip_address',
		),
		'transaction_id'       => array(
			'type' => 'string',
			'name' => 'transaction_id',
		),
		'user_agent'           => array(
			'type' => 'string',
			'name' => 'customer_user_agent',
		),
		'customer_note'        => array(
			'type' => 'string',
			'name' => 'customer_note',
		),
	);

	/**
	 * Table column to WC_Order mapping for billing addresses in wc_address table.
	 *
	 * @var \string[][]
	 */
	protected $billing_address_column_mapping = array(
		'id'           => array( 'type' => 'int' ),
		'order_id'     => array( 'type' => 'int' ),
		'address_type' => array( 'type' => 'string' ),
		'first_name'   => array(
			'type' => 'string',
			'name' => 'billing_first_name',
		),
		'last_name'    => array(
			'type' => 'string',
			'name' => 'billing_last_name',
		),
		'company'      => array(
			'type' => 'string',
			'name' => 'billing_company',
		),
		'address_1'    => array(
			'type' => 'string',
			'name' => 'billing_address_1',
		),
		'address_2'    => array(
			'type' => 'string',
			'name' => 'billing_address_2',
		),
		'city'         => array(
			'type' => 'string',
			'name' => 'billing_city',
		),
		'state'        => array(
			'type' => 'string',
			'name' => 'billing_state',
		),
		'postcode'     => array(
			'type' => 'string',
			'name' => 'billing_postcode',
		),
		'country'      => array(
			'type' => 'string',
			'name' => 'billing_country',
		),
		'email'        => array(
			'type' => 'string',
			'name' => 'billing_email',
		),
		'phone'        => array(
			'type' => 'string',
			'name' => 'billing_phone',
		),
	);

	/**
	 * Table column to WC_Order mapping for shipping addresses in wc_address table.
	 *
	 * @var \string[][]
	 */
	protected $shipping_address_column_mapping = array(
		'id'           => array( 'type' => 'int' ),
		'order_id'     => array( 'type' => 'int' ),
		'address_type' => array( 'type' => 'string' ),
		'first_name'   => array(
			'type' => 'string',
			'name' => 'shipping_first_name',
		),
		'last_name'    => array(
			'type' => 'string',
			'name' => 'shipping_last_name',
		),
		'company'      => array(
			'type' => 'string',
			'name' => 'shipping_company',
		),
		'address_1'    => array(
			'type' => 'string',
			'name' => 'shipping_address_1',
		),
		'address_2'    => array(
			'type' => 'string',
			'name' => 'shipping_address_2',
		),
		'city'         => array(
			'type' => 'string',
			'name' => 'shipping_city',
		),
		'state'        => array(
			'type' => 'string',
			'name' => 'shipping_state',
		),
		'postcode'     => array(
			'type' => 'string',
			'name' => 'shipping_postcode',
		),
		'country'      => array(
			'type' => 'string',
			'name' => 'shipping_country',
		),
		'email'        => array( 'type' => 'string' ),
		'phone'        => array(
			'type' => 'string',
			'name' => 'shipping_phone',
		),
	);

	/**
	 * Table column to WC_Order mapping for wc_operational_data table.
	 *
	 * @var \string[][]
	 */
	protected $operational_data_column_mapping = array(
		'id'                          => array( 'type' => 'int' ),
		'order_id'                    => array( 'type' => 'int' ),
		'created_via'                 => array(
			'type' => 'string',
			'name' => 'created_via',
		),
		'woocommerce_version'         => array(
			'type' => 'string',
			'name' => 'version',
		),
		'prices_include_tax'          => array(
			'type' => 'bool',
			'name' => 'prices_include_tax',
		),
		'coupon_usages_are_counted'   => array(
			'type' => 'bool',
			'name' => 'recorded_coupon_usage_counts',
		),
		'download_permission_granted' => array(
			'type' => 'bool',
			'name' => 'download_permissions_granted',
		),
		'cart_hash'                   => array(
			'type' => 'string',
			'name' => 'cart_hash',
		),
		'new_order_email_sent'        => array(
			'type' => 'bool',
			'name' => 'new_order_email_sent',
		),
		'order_key'                   => array(
			'type' => 'string',
			'name' => 'order_key',
		),
		'order_stock_reduced'         => array(
			'type' => 'bool',
			'name' => 'order_stock_reduced',
		),
		'date_paid_gmt'               => array(
			'type' => 'date',
			'name' => 'date_paid',
		),
		'date_completed_gmt'          => array(
			'type' => 'date',
			'name' => 'date_completed',
		),
		'shipping_tax_amount'         => array(
			'type' => 'decimal',
			'name' => 'shipping_tax',
		),
		'shipping_total_amount'       => array(
			'type' => 'decimal',
			'name' => 'shipping_total',
		),
		'discount_tax_amount'         => array(
			'type' => 'decimal',
			'name' => 'discount_tax',
		),
		'discount_total_amount'       => array(
			'type' => 'decimal',
			'name' => 'discount_total',
		),
		'recorded_sales'              => array(
			'type' => 'bool',
			'name' => 'recorded_sales',
		),
	);

	/**
	 * Cache variable to store combined mapping.
	 *
	 * @var array[][][]
	 */
	private $all_order_column_mapping;

	/**
	 * Return combined mappings for all order tables.
	 *
	 * @return array|\array[][][] Return combined mapping.
	 */
	public function get_all_order_column_mappings() {
		if ( ! isset( $this->all_order_column_mapping ) ) {
			$this->all_order_column_mapping = array(
				'orders'           => $this->order_column_mapping,
				'billing_address'  => $this->billing_address_column_mapping,
				'shipping_address' => $this->shipping_address_column_mapping,
				'operational_data' => $this->operational_data_column_mapping,
			);
		}

		return $this->all_order_column_mapping;
	}

	/**
	 * Helper function to get alias for op table, this is used in select query.
	 *
	 * @return string Alias.
	 */
	private function get_op_table_alias() : string {
		return 'order_operational_data';
	}

	/**
	 * Helper function to get alias for address table, this is used in select query.
	 *
	 * @param string $type Address type.
	 *
	 * @return string Alias.
	 */
	private function get_address_table_alias( string $type ) : string {
		return "address_$type";
	}

	/**
	 * Helper method to get a CPT data store instance to use.
	 *
	 * @return \WC_Order_Data_Store_CPT Data store instance.
	 */
	public function get_cpt_data_store_instance() {
		if ( ! isset( $this->cpt_data_store ) ) {
			$this->cpt_data_store = $this->get_post_data_store_for_backfill();
		}
		return $this->cpt_data_store;
	}


	/**
	 * Returns data store object to use backfilling.
	 *
	 * @return \Abstract_WC_Order_Data_Store_CPT
	 */
	protected function get_post_data_store_for_backfill() {
		return new \WC_Order_Data_Store_CPT();
	}

	/**
	 * Backfills order details in to WP_Post DB. Uses WC_Order_Data_store_CPT.
	 *
	 * @param \WC_Abstract_Order $order Order object to backfill.
	 */
	public function backfill_post_record( $order ) {
		$cpt_data_store = $this->get_post_data_store_for_backfill();
		if ( is_null( $cpt_data_store ) || ! method_exists( $cpt_data_store, 'update_order_from_object' ) ) {
			return;
		}

		$cpt_data_store->update_order_from_object( $order );
		foreach ( $cpt_data_store->get_internal_data_store_key_getters() as $key => $getter_name ) {
			if (
				is_callable( array( $cpt_data_store, "set_$getter_name" ) ) &&
				is_callable( array( $this, "get_$getter_name" ) )
			) {
				call_user_func_array(
					array(
						$cpt_data_store,
						"set_$getter_name",
					),
					array(
						$order,
						$this->{"get_$getter_name"}( $order ),
					)
				);
			}
		}
	}

	/**
	 * Get information about whether permissions are granted yet.
	 *
	 * @param \WC_Order $order Order object.
	 *
	 * @return bool Whether permissions are granted.
	 */
	public function get_download_permissions_granted( $order ) {
		$order = is_int( $order ) ? wc_get_order( $order ) : $order;
		return $order->get_download_permissions_granted();
	}

	/**
	 * Stores information about whether permissions were generated yet.
	 *
	 * @param \WC_Order $order Order ID or order object.
	 * @param bool      $set True or false.
	 */
	public function set_download_permissions_granted( $order, $set ) {
		if ( is_int( $order ) ) {
			$order = wc_get_order( $order );
		}
		$order->set_download_permissions_granted( $set );
		$order->save();
	}

	/**
	 * Gets information about whether sales were recorded.
	 *
	 * @param \WC_Order $order Order object.
	 *
	 * @return bool Whether sales are recorded.
	 */
	public function get_recorded_sales( $order ) {
		$order = is_int( $order ) ? wc_get_order( $order ) : $order;
		return $order->get_recorded_sales();
	}

	/**
	 * Stores information about whether sales were recorded.
	 *
	 * @param \WC_Order $order Order object.
	 * @param bool      $set True or false.
	 */
	public function set_recorded_sales( $order, $set ) {
		if ( is_int( $order ) ) {
			$order = wc_get_order( $order );
		}
		$order->set_recorded_sales( $set );
		$order->save();
	}

	/**
	 * Gets information about whether coupon counts were updated.
	 *
	 * @param \WC_Order $order Order object.
	 *
	 * @return bool Whether coupon counts were updated.
	 */
	public function get_recorded_coupon_usage_counts( $order ) {
		$order = is_int( $order ) ? wc_get_order( $order ) : $order;
		return $order->get_recorded_coupon_usage_counts();
	}

	/**
	 * Stores information about whether coupon counts were updated.
	 *
	 * @param \WC_Order $order Order object.
	 * @param bool      $set True or false.
	 */
	public function set_recorded_coupon_usage_counts( $order, $set ) {
		if ( is_int( $order ) ) {
			$order = wc_get_order( $order );
		}
		$order->set_recorded_coupon_usage_counts( $set );
		$order->save();
	}

	/**
	 * Whether email have been sent for this order.
	 *
	 * @param \WC_Order|int $order Order object.
	 *
	 * @return bool Whether email is sent.
	 */
	public function get_email_sent( $order ) {
		$order = is_int( $order ) ? wc_get_order( $order ) : $order;
		return $order->get_new_order_email_sent();
	}

	/**
	 * Stores information about whether email was sent.
	 *
	 * @param \WC_Order $order Order object.
	 * @param bool      $set True or false.
	 */
	public function set_email_sent( $order, $set ) {
		if ( is_int( $order ) ) {
			$order = wc_get_order( $order );
		}
		$order->set_new_order_email_sent( $set );
		$order->save();
	}

	/**
	 * Helper setter for email_sent.
	 *
	 * @param \WC_Order $order Order object.
	 *
	 * @return bool Whether email was sent.
	 */
	public function get_new_order_email_sent( $order ) {
		$order = is_int( $order ) ? wc_get_order( $order ) : $order;
		return $order->get_new_order_email_sent();
	}

	/**
	 * Helper setter for new order email sent.
	 *
	 * @param \WC_Order $order Order object.
	 * @param bool      $set True or false.
	 */
	public function set_new_order_email_sent( $order, $set ) {
		if ( is_int( $order ) ) {
			$order = wc_get_order( $order );
		}
		$order->set_new_order_email_sent( $set );
		$order->save();
	}

	/**
	 * Gets information about whether stock was reduced.
	 *
	 * @param \WC_Order $order Order object.
	 *
	 * @return bool Whether stock was reduced.
	 */
	public function get_stock_reduced( $order ) {
		$order = is_int( $order ) ? wc_get_order( $order ) : $order;
		return $order->get_order_stock_reduced();
	}

	/**
	 * Stores information about whether stock was reduced.
	 *
	 * @param \WC_Order $order Order ID or order object.
	 * @param bool      $set True or false.
	 */
	public function set_stock_reduced( $order, $set ) {
		if ( is_int( $order ) ) {
			$order = wc_get_order( $order );
		}
		$order->set_order_stock_reduced( $set );
		$order->save();
	}

	/**
	 * Helper getter for `order_stock_reduced`.
	 *
	 * @param \WC_Order $order Order object.
	 * @return bool Whether stock was reduced.
	 */
	public function get_order_stock_reduced( $order ) {
		return $this->get_stock_reduced( $order );
	}

	/**
	 * Helper setter for `order_stock_reduced`.
	 *
	 * @param \WC_Order $order Order ID or order object.
	 * @param bool      $set Whether stock was reduced.
	 */
	public function set_order_stock_reduced( $order, $set ) {
		$this->set_stock_reduced( $order, $set );
	}

	/**
	 * Get amount already refunded.
	 *
	 * @param \WC_Order $order Order object.
	 *
	 * @return float Refunded amount.
	 */
	public function get_total_refunded( $order ) {
		global $wpdb;
		$order_table = self::get_orders_table_name();
		$total       = $wpdb->get_var(
			$wpdb->prepare(
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
				"
SELECT SUM( total_amount ) FROM $order_table
WHERE
    type = %s AND
    parent_order_id = %d
;
",
				// phpcs:enable
				'shop_order_refund',
				$order->get_id()
			)
		);
		return -1 * ( isset( $total ) ? $total : 0 );
	}

	/**
	 * Get the total tax refunded.
	 *
	 * @param  WC_Order $order Order object.
	 * @return float
	 */
	public function get_total_tax_refunded( $order ) {
		global $wpdb;

		$order_table = self::get_orders_table_name();

		$total = $wpdb->get_var(
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
			$wpdb->prepare(
				"SELECT SUM( order_itemmeta.meta_value )
				FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
				INNER JOIN $order_table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d )
				INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = orders.id AND order_items.order_item_type = 'tax' )
				WHERE order_itemmeta.order_item_id = order_items.order_item_id
				AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')",
				$order->get_id()
			)
		) ?? 0;
		// phpcs:enable

		return abs( $total );
	}

	/**
	 * Get the total shipping refunded.
	 *
	 * @param  WC_Order $order Order object.
	 * @return float
	 */
	public function get_total_shipping_refunded( $order ) {
		global $wpdb;

		$order_table = self::get_orders_table_name();

		$total = $wpdb->get_var(
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
			$wpdb->prepare(
				"SELECT SUM( order_itemmeta.meta_value )
				FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
				INNER JOIN $order_table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d )
				INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = orders.id AND order_items.order_item_type = 'shipping' )
				WHERE order_itemmeta.order_item_id = order_items.order_item_id
				AND order_itemmeta.meta_key IN ('cost')",
				$order->get_id()
			)
		) ?? 0;
		// phpcs:enable

		return abs( $total );
	}

	/**
	 * Finds an Order ID based on an order key.
	 *
	 * @param string $order_key An order key has generated by.
	 * @return int The ID of an order, or 0 if the order could not be found
	 */
	public function get_order_id_by_order_key( $order_key ) {
		global $wpdb;

		$orders_table = self::get_orders_table_name();
		$op_table     = self::get_operational_data_table_name();

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT {$orders_table}.id FROM {$orders_table}
				INNER JOIN {$op_table} ON {$op_table}.order_id = {$orders_table}.id
				WHERE {$op_table}.order_key = %s",
				$order_key
			)
		);
		// phpcs:enable
	}

	/**
	 * Return count of orders with a specific status.
	 *
	 * @param  string $status Order status. Function wc_get_order_statuses() returns a list of valid statuses.
	 * @return int
	 */
	public function get_order_count( $status ) {
		global $wpdb;

		$orders_table = self::get_orders_table_name();

		return absint( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$orders_table} WHERE type = %s AND status = %s", 'shop_order', $status ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Get all orders matching the passed in args.
	 *
	 * @deprecated 3.1.0 - Use {@see wc_get_orders} instead.
	 * @param  array $args List of args passed to wc_get_orders().
	 * @return array|object
	 */
	public function get_orders( $args = array() ) {
		wc_deprecated_function( __METHOD__, '3.1.0', 'Use wc_get_orders instead.' );
		return wc_get_orders( $args );
	}

	/**
	 * Get unpaid orders last updated before the specified date.
	 *
	 * @param  int $date Timestamp.
	 * @return array
	 */
	public function get_unpaid_orders( $date ) {
		global $wpdb;

		$orders_table    = self::get_orders_table_name();
		$order_types_sql = "('" . implode( "','", wc_get_order_types() ) . "')";

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM {$orders_table} WHERE
				{$orders_table}.type IN {$order_types_sql}
				AND {$orders_table}.status = %s
				AND {$orders_table}.date_updated_gmt < %s",
				'wc-pending',
				gmdate( 'Y-m-d H:i:s', absint( $date ) )
			)
		);
		// phpcs:enable
	}

	/**
	 * Search order data for a term and return matching order IDs.
	 *
	 * @param string $term Search term.
	 *
	 * @return int[] Array of order IDs.
	 */
	public function search_orders( $term ) {
		$order_ids = wc_get_orders( array( 's' => $term ) );

		/**
		 * Provides an opportunity to modify the list of order IDs obtained during an order search.
		 *
		 * This hook is used for Custom Order Table queries. For Custom Post Type order searches, the corresponding hook
		 * is `woocommerce_shop_order_search_results`.
		 *
		 * @since 7.0.0
		 *
		 * @param int[]  $order_ids Search results as an array of order IDs.
		 * @param string $term      The search term.
		 */
		return array_map( 'intval', (array) apply_filters( 'woocommerce_cot_shop_order_search_results', $order_ids, $term ) );
	}

	//phpcs:enable Squiz.Commenting, Generic.Commenting

	/**
	 * Fetch order type for orders in bulk.
	 *
	 * @param array $order_ids Order IDs.
	 *
	 * @return array array( $order_id1 => $type1, ... ) Array for all orders.
	 */
	public function get_orders_type( $order_ids ) {
		global $wpdb;

		if ( empty( $order_ids ) ) {
			return array();
		}

		$orders_table          = self::get_orders_table_name();
		$order_ids_placeholder = implode( ', ', array_fill( 0, count( $order_ids ), '%d' ) );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, type FROM {$orders_table} WHERE id IN ( $order_ids_placeholder )",
				$order_ids
			)
		);
		// phpcs:enable
		$order_types = array();
		foreach ( $results as $row ) {
			$order_types[ $row->id ] = $row->type;
		}
		return $order_types;
	}

	/**
	 * Get order type from DB.
	 *
	 * @param int $order_id Order ID.
	 *
	 * @return string Order type.
	 */
	public function get_order_type( $order_id ) {
		$type = $this->get_orders_type( array( $order_id ) );
		return $type[ $order_id ] ?? '';
	}

	/**
	 * Method to read an order from custom tables.
	 *
	 * @param \WC_Order $order Order object.
	 *
	 * @throws \Exception If passed order is invalid.
	 */
	public function read( &$order ) {
		$orders_array = array( $order->get_id() => $order );
		$this->read_multiple( $orders_array );
	}

	/**
	 * Reads multiple orders from custom tables in one pass.
	 *
	 * @since 6.9.0
	 * @param array[\WC_Order] $orders Order objects.
	 * @throws \Exception If passed an invalid order.
	 */
	public function read_multiple( &$orders ) {
		$order_ids = array_keys( $orders );
		$data      = $this->get_order_data_for_ids( $order_ids );

		if ( count( $data ) !== count( $order_ids ) ) {
			throw new \Exception( __( 'Invalid order IDs in call to read_multiple()', 'woocommerce' ) );
		}

		$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
		if ( ! $data_synchronizer instanceof DataSynchronizer ) {
			return;
		}

		$data_sync_enabled = $data_synchronizer->data_sync_is_enabled() && 0 === $data_synchronizer->get_current_orders_pending_sync_count_cached();
		$load_posts_for    = array_diff( $order_ids, self::$reading_order_ids );
		$post_orders       = $data_sync_enabled ? $this->get_post_orders_for_ids( array_intersect_key( $orders, array_flip( $load_posts_for ) ) ) : array();

		foreach ( $data as $order_data ) {
			$order_id = absint( $order_data->id );
			$order    = $orders[ $order_id ];

			$this->init_order_record( $order, $order_id, $order_data );

			if ( $data_sync_enabled && $this->should_sync_order( $order ) && isset( $post_orders[ $order_id ] ) ) {
				self::$reading_order_ids[] = $order_id;
				$this->maybe_sync_order( $order, $post_orders[ $order->get_id() ] );
			}
		}
	}

	/**
	 * Helper method to check whether to sync the order.
	 *
	 * @param \WC_Abstract_Order $order Order object.
	 *
	 * @return bool Whether the order should be synced.
	 */
	private function should_sync_order( \WC_Abstract_Order $order ) : bool {
		$draft_order    = in_array( $order->get_status(), array( 'draft', 'auto-draft' ), true );
		$already_synced = in_array( $order->get_id(), self::$reading_order_ids, true );
		return ! $draft_order && ! $already_synced;
	}

	/**
	 * Helper method to initialize order object from DB data.
	 *
	 * @param \WC_Abstract_Order $order Order object.
	 * @param int                $order_id Order ID.
	 * @param \stdClass          $order_data Order data fetched from DB.
	 *
	 * @return void
	 */
	private function init_order_record( \WC_Abstract_Order &$order, int $order_id, \stdClass $order_data ) {
		$order->set_defaults();
		$order->set_id( $order_id );
		$filtered_meta_data = $this->filter_raw_meta_data( $order, $order_data->meta_data );
		$order->init_meta_data( $filtered_meta_data );
		$this->set_order_props_from_data( $order, $order_data );
		$order->set_object_read( true );
	}

	/**
	 * For post based data stores, this was used to filter internal meta data. For custom tables, technically there is no internal meta data,
	 * (i.e. we store all core data as properties for the order, and not in meta data). So this method is a no-op.
	 *
	 * Except that some meta such as billing_address_index and shipping_address_index are infact stored in meta data, so we need to filter those out.
	 *
	 * However, declaring $internal_meta_keys is still required so that our backfill and other comparison checks works as expected.
	 *
	 * @param \WC_Data $object Object to filter meta data for.
	 * @param array    $raw_meta_data Raw meta data.
	 *
	 * @return array Filtered meta data.
	 */
	public function filter_raw_meta_data( &$object, $raw_meta_data ) {
		$filtered_meta_data = parent::filter_raw_meta_data( $object, $raw_meta_data );
		$allowed_keys       = array(
			'_billing_address_index',
			'_shipping_address_index',
		);
		$allowed_meta       = array_filter(
			$raw_meta_data,
			function( $meta ) use ( $allowed_keys ) {
				return in_array( $meta->meta_key, $allowed_keys, true );
			}
		);

		return array_merge( $allowed_meta, $filtered_meta_data );
	}

	/**
	 * Sync order to/from posts tables if we are able to detect difference between order and posts but the sync is enabled.
	 *
	 * @param \WC_Abstract_Order $order Order object.
	 * @param \WC_Abstract_Order $post_order Order object initialized from post.
	 *
	 * @return void
	 * @throws \Exception If passed an invalid order.
	 */
	private function maybe_sync_order( \WC_Abstract_Order &$order, \WC_Abstract_Order $post_order ) {
		if ( ! $this->is_post_different_from_order( $order, $post_order ) ) {
			return;
		}

		// Modified dates can be empty when the order is created but never updated again. Fallback to created date in those cases.
		$order_modified_date      = $order->get_date_modified() ?? $order->get_date_created();
		$order_modified_date      = is_null( $order_modified_date ) ? 0 : $order_modified_date->getTimestamp();
		$post_order_modified_date = $post_order->get_date_modified() ?? $post_order->get_date_created();
		$post_order_modified_date = is_null( $post_order_modified_date ) ? 0 : $post_order_modified_date->getTimestamp();

		/**
		 * We are here because there was difference in posts and order data, although the sync is enabled.
		 * When order modified date is more recent than post modified date, it can only mean that COT definitely has more updated version of the order.
		 *
		 * In a case where post meta was updated (without updating post_modified date), post_modified would be equal to order_modified date.
		 *
		 * So we write back to the order table when order modified date is more recent than post modified date. Otherwise, we write to the post table.
		 */
		if ( $post_order_modified_date >= $order_modified_date ) {
			$this->migrate_post_record( $order, $post_order );
		}
	}

	/**
	 * Get the post type order representation.
	 *
	 * @param \WP_Post $post Post object.
	 *
	 * @return \WC_Order Order object.
	 */
	private function get_cpt_order( $post ) {
		$cpt_order = new \WC_Order();
		$cpt_order->set_id( $post->ID );
		$cpt_data_store = $this->get_cpt_data_store_instance();
		$cpt_data_store->read( $cpt_order );
		return $cpt_order;
	}

	/**
	 * Helper function to get posts data for an order in bullk. We use to this to compute posts object in bulk so that we can compare it with COT data.
	 *
	 * @param array $orders    List of orders mapped by $order_id.
	 *
	 * @return array List of posts.
	 */
	private function get_post_orders_for_ids( array $orders ): array {
		$order_ids = array_keys( $orders );
		// We have to bust meta cache, otherwise we will just get the meta cached by OrderTableDataStore.
		foreach ( $order_ids as $order_id ) {
			wp_cache_delete( WC_Order::generate_meta_cache_key( $order_id, 'orders' ), 'orders' );
		}

		$cpt_stores       = array();
		$cpt_store_orders = array();
		foreach ( $orders as $order_id => $order ) {
			$table_data_store     = $order->get_data_store();
			$cpt_data_store       = $table_data_store->get_cpt_data_store_instance();
			$cpt_store_class_name = get_class( $cpt_data_store );
			if ( ! isset( $cpt_stores[ $cpt_store_class_name ] ) ) {
				$cpt_stores[ $cpt_store_class_name ]       = $cpt_data_store;
				$cpt_store_orders[ $cpt_store_class_name ] = array();
			}
			$cpt_store_orders[ $cpt_store_class_name ][ $order_id ] = $order;
		}

		$cpt_orders = array();
		foreach ( $cpt_stores as $cpt_store_name => $cpt_store ) {
			// Prime caches if we can.
			if ( method_exists( $cpt_store, 'prime_caches_for_orders' ) ) {
				$cpt_store->prime_caches_for_orders( array_keys( $cpt_store_orders[ $cpt_store_name ] ), array() );
			}

			foreach ( $cpt_store_orders[ $cpt_store_name ] as $order_id => $order ) {
				$cpt_order_class_name = wc_get_order_type( $order->get_type() )['class_name'];
				$cpt_order            = new $cpt_order_class_name();
				$cpt_order->set_id( $order_id );
				$cpt_store->read( $cpt_order );
				$cpt_orders[ $order_id ] = $cpt_order;
			}
		}
		return $cpt_orders;
	}

	/**
	 * Computes whether post has been updated after last order. Tries to do it as efficiently as possible.
	 *
	 * @param \WC_Abstract_Order $order Order object.
	 * @param \WC_Abstract_Order $post_order Order object read from posts table.
	 *
	 * @return bool True if post is different than order.
	 */
	private function is_post_different_from_order( $order, $post_order ): bool {
		if ( ArrayUtil::deep_compare_array_diff( $order->get_base_data(), $post_order->get_base_data(), false ) ) {
			return true;
		}

		$meta_diff = $this->get_diff_meta_data_between_orders( $order, $post_order );
		if ( ! empty( $meta_diff ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Migrate meta data from post to order.
	 *
	 * @param \WC_Abstract_Order $order Order object.
	 * @param \WC_Abstract_Order $post_order Order object read from posts table.
	 *
	 * @return array List of meta data that was migrated.
	 */
	private function migrate_meta_data_from_post_order( \WC_Abstract_Order &$order, \WC_Abstract_Order $post_order ) {
		$diff = $this->get_diff_meta_data_between_orders( $order, $post_order, true );
		$order->save_meta_data();
		return $diff;
	}

	/**
	 * Helper function to compute diff between metadata of post and cot data for an order.
	 *
	 * Also provides an option to sync the metadata as well, since we are already computing the diff.
	 *
	 * @param \WC_Abstract_Order $order1 Order object read from posts.
	 * @param \WC_Abstract_Order $order2 Order object read from COT.
	 * @param bool               $sync   Whether to also sync the meta data.
	 *
	 * @return array Difference between post and COT meta data.
	 */
	private function get_diff_meta_data_between_orders( \WC_Abstract_Order &$order1, \WC_Abstract_Order $order2, $sync = false ): array {
		$order1_meta        = ArrayUtil::select( $order1->get_meta_data(), 'get_data', ArrayUtil::SELECT_BY_OBJECT_METHOD );
		$order2_meta        = ArrayUtil::select( $order2->get_meta_data(), 'get_data', ArrayUtil::SELECT_BY_OBJECT_METHOD );
		$order1_meta_by_key = ArrayUtil::select_as_assoc( $order1_meta, 'key', ArrayUtil::SELECT_BY_ARRAY_KEY );
		$order2_meta_by_key = ArrayUtil::select_as_assoc( $order2_meta, 'key', ArrayUtil::SELECT_BY_ARRAY_KEY );

		$diff = array();
		foreach ( $order1_meta_by_key as $key => $value ) {
			if ( in_array( $key, $this->internal_meta_keys, true ) ) {
				// These should have already been verified in the base data comparison.
				continue;
			}
			$order1_values = ArrayUtil::select( $value, 'value', ArrayUtil::SELECT_BY_ARRAY_KEY );
			if ( ! array_key_exists( $key, $order2_meta_by_key ) ) {
				$sync && $order1->delete_meta_data( $key );
				$diff[ $key ] = $order1_values;
				unset( $order2_meta_by_key[ $key ] );
				continue;
			}

			$order2_values = ArrayUtil::select( $order2_meta_by_key[ $key ], 'value', ArrayUtil::SELECT_BY_ARRAY_KEY );
			$new_diff      = ArrayUtil::deep_assoc_array_diff( $order1_values, $order2_values );
			if ( ! empty( $new_diff ) && $sync ) {
				if ( count( $order2_values ) > 1 ) {
					$sync && $order1->delete_meta_data( $key );
					foreach ( $order2_values as $post_order_value ) {
						$sync && $order1->add_meta_data( $key, $post_order_value, false );
					}
				} else {
					$sync && $order1->update_meta_data( $key, $order2_values[0] );
				}
				$diff[ $key ] = $new_diff;
				unset( $order2_meta_by_key[ $key ] );
			}
		}

		foreach ( $order2_meta_by_key as $key => $value ) {
			if ( array_key_exists( $key, $order1_meta_by_key ) || in_array( $key, $this->internal_meta_keys, true ) ) {
				continue;
			}
			$order2_values = ArrayUtil::select( $value, 'value', ArrayUtil::SELECT_BY_ARRAY_KEY );
			foreach ( $order2_values as $meta_value ) {
				$sync && $order1->add_meta_data( $key, $meta_value );
			}
			$diff[ $key ] = $order2_values;
		}
		return $diff;
	}

	/**
	 * Log difference between post and COT data for an order.
	 *
	 * @param array $diff Difference between post and COT data.
	 *
	 * @return void
	 */
	private function log_diff( array $diff ): void {
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- This is a log function.
		$this->error_logger->notice( 'Diff found: ' . print_r( $diff, true ) );
	}

	/**
	 * Migrate post record from a given order object.
	 *
	 * @param \WC_Abstract_Order $order Order object.
	 * @param \WC_Abstract_Order $post_order Order object read from posts.
	 *
	 * @return void
	 */
	private function migrate_post_record( \WC_Abstract_Order &$order, \WC_Abstract_Order $post_order ): void {
		$this->migrate_meta_data_from_post_order( $order, $post_order );
		$post_order_base_data = $post_order->get_base_data();
		foreach ( $post_order_base_data as $key => $value ) {
			$this->set_order_prop( $order, $key, $value );
		}
		$this->persist_updates( $order, false );
	}

	/**
	 * Sets order properties based on a row from the database.
	 *
	 * @param \WC_Abstract_Order $order      The order object.
	 * @param object             $order_data A row of order data from the database.
	 */
	private function set_order_props_from_data( &$order, $order_data ) {
		foreach ( $this->get_all_order_column_mappings() as $table_name => $column_mapping ) {
			foreach ( $column_mapping as $column_name => $prop_details ) {
				if ( ! isset( $prop_details['name'] ) ) {
					continue;
				}
				$prop_value = $order_data->{$prop_details['name']};
				if ( is_null( $prop_value ) ) {
					continue;
				}

				if ( 'date' === $prop_details['type'] ) {
					$prop_value = $this->string_to_timestamp( $prop_value );
				}

				$this->set_order_prop( $order, $prop_details['name'], $prop_value );
			}
		}
	}

	/**
	 * Set order prop if a setter exists in either the order object or in the data store.
	 *
	 * @param \WC_Abstract_Order $order Order object.
	 * @param string             $prop_name Property name.
	 * @param mixed              $prop_value Property value.
	 *
	 * @return bool True if the property was set, false otherwise.
	 */
	private function set_order_prop( \WC_Abstract_Order $order, string $prop_name, $prop_value ) {
		$prop_setter_function_name = "set_{$prop_name}";
		if ( is_callable( array( $order, $prop_setter_function_name ) ) ) {
			return $order->{$prop_setter_function_name}( $prop_value );
		} elseif ( is_callable( array( $this, $prop_setter_function_name ) ) ) {
			return $this->{$prop_setter_function_name}( $order, $prop_value, false );
		}
		return false;
	}

	/**
	 * Return order data for a single order ID.
	 *
	 * @param int $id Order ID.
	 *
	 * @return object|\WP_Error DB order object or WP_Error.
	 */
	private function get_order_data_for_id( $id ) {
		$results = $this->get_order_data_for_ids( array( $id ) );

		return is_array( $results ) && count( $results ) > 0 ? $results[ $id ] : $results;
	}

	/**
	 * Return order data for multiple IDs.
	 *
	 * @param array $ids List of order IDs.
	 *
	 * @return \stdClass[]|object|null DB Order objects or error.
	 */
	private function get_order_data_for_ids( $ids ) {
		if ( ! $ids ) {
			return array();
		}

		global $wpdb;
		if ( empty( $ids ) ) {
			return array();
		}
		$order_table_query = $this->get_order_table_select_statement();
		$id_placeholder    = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
		$order_meta_table  = self::get_meta_table_name();

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_table_query is autogenerated and should already be prepared.
		$table_data = $wpdb->get_results(
			$wpdb->prepare(
				"$order_table_query WHERE wc_order.id in ( $id_placeholder )",
				$ids
			)
		);
		// phpcs:enable

		$meta_data_query = $this->get_order_meta_select_statement();
		$order_data      = array();
		$meta_data       = $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $meta_data_query and $order_meta_table is autogenerated and should already be prepared. $id_placeholder is already prepared.
				"$meta_data_query WHERE $order_meta_table.order_id in ( $id_placeholder )",
				$ids
			)
		);
		foreach ( $table_data as $table_datum ) {
			$order_data[ $table_datum->id ]            = $table_datum;
			$order_data[ $table_datum->id ]->meta_data = array();
		}

		foreach ( $meta_data as $meta_datum ) {
			// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Not a meta query.
			$order_data[ $meta_datum->order_id ]->meta_data[] = (object) array(
				'meta_id'    => $meta_datum->id,
				'meta_key'   => $meta_datum->meta_key,
				'meta_value' => $meta_datum->meta_value,
			);
			// phpcs:enable
		}
		return $order_data;
	}

	/**
	 * Helper method to generate combined select statement.
	 *
	 * @return string Select SQL statement to fetch order.
	 */
	private function get_order_table_select_statement() {
		$order_table                  = $this::get_orders_table_name();
		$order_table_alias            = 'wc_order';
		$select_clause                = $this->generate_select_clause_for_props( $order_table_alias, $this->order_column_mapping );
		$billing_address_table_alias  = $this->get_address_table_alias( 'billing' );
		$shipping_address_table_alias = $this->get_address_table_alias( 'shipping' );
		$op_data_table_alias          = $this->get_op_table_alias();
		$billing_address_clauses      = $this->join_billing_address_table_to_order_query( $order_table_alias, $billing_address_table_alias );
		$shipping_address_clauses     = $this->join_shipping_address_table_to_order_query( $order_table_alias, $shipping_address_table_alias );
		$operational_data_clauses     = $this->join_operational_data_table_to_order_query( $order_table_alias, $op_data_table_alias );

		return "
SELECT $select_clause, {$billing_address_clauses['select']}, {$shipping_address_clauses['select']}, {$operational_data_clauses['select']}
FROM $order_table $order_table_alias
LEFT JOIN {$billing_address_clauses['join']}
LEFT JOIN {$shipping_address_clauses['join']}
LEFT JOIN {$operational_data_clauses['join']}
";
	}

	/**
	 * Helper function to generate select statement for fetching metadata in bulk.
	 *
	 * @return string Select SQL statement to fetch order metadata.
	 */
	private function get_order_meta_select_statement() {
		$order_meta_table = self::get_meta_table_name();
		return "
SELECT $order_meta_table.id, $order_meta_table.order_id, $order_meta_table.meta_key, $order_meta_table.meta_value
FROM $order_meta_table
		";
	}

	/**
	 * Helper method to generate join query for billing addresses in wc_address table.
	 *
	 * @param string $order_table_alias Alias for order table to use in join.
	 * @param string $address_table_alias Alias for address table to use in join.
	 *
	 * @return array Select and join statements for billing address table.
	 */
	private function join_billing_address_table_to_order_query( $order_table_alias, $address_table_alias ) {
		return $this->join_address_table_order_query( 'billing', $order_table_alias, $address_table_alias );
	}

	/**
	 * Helper method to generate join query for shipping addresses in wc_address table.
	 *
	 * @param string $order_table_alias Alias for order table to use in join.
	 * @param string $address_table_alias Alias for address table to use in join.
	 *
	 * @return array Select and join statements for shipping address table.
	 */
	private function join_shipping_address_table_to_order_query( $order_table_alias, $address_table_alias ) {
		return $this->join_address_table_order_query( 'shipping', $order_table_alias, $address_table_alias );
	}

	/**
	 * Helper method to generate join and select query for address table.
	 *
	 * @param string $address_type Type of address. Typically will be `billing` or `shipping`.
	 * @param string $order_table_alias Alias of order table to use.
	 * @param string $address_table_alias Alias for address table to use.
	 *
	 * @return array Select and join statements for address table.
	 */
	private function join_address_table_order_query( $address_type, $order_table_alias, $address_table_alias ) {
		global $wpdb;
		$address_table    = $this::get_addresses_table_name();
		$column_props_map = 'billing' === $address_type ? $this->billing_address_column_mapping : $this->shipping_address_column_mapping;
		$clauses          = $this->generate_select_and_join_clauses( $order_table_alias, $address_table, $address_table_alias, $column_props_map );
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $clauses['join'] and $address_table_alias are hardcoded.
		$clauses['join'] = $wpdb->prepare(
			"{$clauses['join']} AND $address_table_alias.address_type = %s",
			$address_type
		);

		// phpcs:enable
		return array(
			'select' => $clauses['select'],
			'join'   => $clauses['join'],
		);
	}

	/**
	 * Helper method to join order operational data table.
	 *
	 * @param string $order_table_alias Alias to use for order table.
	 * @param string $operational_table_alias Alias to use for operational data table.
	 *
	 * @return array Select and join queries for operational data table.
	 */
	private function join_operational_data_table_to_order_query( $order_table_alias, $operational_table_alias ) {
		$operational_data_table = $this::get_operational_data_table_name();

		return $this->generate_select_and_join_clauses(
			$order_table_alias,
			$operational_data_table,
			$operational_table_alias,
			$this->operational_data_column_mapping
		);
	}

	/**
	 * Helper method to generate join and select clauses.
	 *
	 * @param string  $order_table_alias Alias for order table.
	 * @param string  $table Table to join.
	 * @param string  $table_alias Alias for table to join.
	 * @param array[] $column_props_map Column to prop map for table to join.
	 *
	 * @return array Select and join queries.
	 */
	private function generate_select_and_join_clauses( $order_table_alias, $table, $table_alias, $column_props_map ) {
		// Add aliases to column names so they will be unique when fetching.
		$select_clause = $this->generate_select_clause_for_props( $table_alias, $column_props_map );
		$join_clause   = "$table $table_alias ON $table_alias.order_id = $order_table_alias.id";

		return array(
			'select' => $select_clause,
			'join'   => $join_clause,
		);
	}

	/**
	 * Helper method to generate select clause for props.
	 *
	 * @param string  $table_alias Alias for table.
	 * @param array[] $props Props to column mapping for table.
	 *
	 * @return string Select clause.
	 */
	private function generate_select_clause_for_props( $table_alias, $props ) {
		$select_clauses = array();
		foreach ( $props as $column_name => $prop_details ) {
			$select_clauses[] = isset( $prop_details['name'] ) ? "$table_alias.$column_name as {$prop_details['name']}" : "$table_alias.$column_name as {$table_alias}_$column_name";
		}

		return implode( ', ', $select_clauses );
	}

	/**
	 * Persists order changes to the database.
	 *
	 * @param \WC_Abstract_Order $order            The order.
	 * @param bool               $force_all_fields Force saving all fields to DB and just changed.
	 *
	 * @throws \Exception If order data is not valid.
	 *
	 * @since 6.8.0
	 */
	protected function persist_order_to_db( &$order, bool $force_all_fields = false ) {
		$context   = ( 0 === absint( $order->get_id() ) ) ? 'create' : 'update';
		$data_sync = wc_get_container()->get( DataSynchronizer::class );

		if ( 'create' === $context ) {
			$post_id = wp_insert_post(
				array(
					'post_type'   => $data_sync->data_sync_is_enabled() ? $order->get_type() : $data_sync::PLACEHOLDER_ORDER_POST_TYPE,
					'post_status' => 'draft',
				)
			);

			if ( ! $post_id ) {
				throw new \Exception( __( 'Could not create order in posts table.', 'woocommerce' ) );
			}

			$order->set_id( $post_id );
		}

		$only_changes = ! $force_all_fields && 'update' === $context;
		// Figure out what needs to be updated in the database.
		$db_updates = $this->get_db_rows_for_order( $order, $context, $only_changes );

		// Persist changes.
		foreach ( $db_updates as $update ) {
			// Make sure 'data' and 'format' entries match before passing to $wpdb.
			ksort( $update['data'] );
			ksort( $update['format'] );

			$result = $this->database_util->insert_on_duplicate_key_update(
				$update['table'],
				$update['data'],
				array_values( $update['format'] )
			);

			if ( false === $result ) {
				// translators: %s is a table name.
				throw new \Exception( sprintf( __( 'Could not persist order to database table "%s".', 'woocommerce' ), $update['table'] ) );
			}
		}

		$changes = $order->get_changes();
		$this->update_address_index_meta( $order, $changes );
	}

	/**
	 * Generates an array of rows with all the details required to insert or update an order in the database.
	 *
	 * @param \WC_Abstract_Order $order The order.
	 * @param string             $context The context: 'create' or 'update'.
	 * @param boolean            $only_changes Whether to consider only changes in the order for generating the rows.
	 *
	 * @return array
	 * @throws \Exception When invalid data is found for the given context.
	 *
	 * @since 6.8.0
	 */
	protected function get_db_rows_for_order( \WC_Abstract_Order $order, string $context = 'create', bool $only_changes = false ): array {
		$result = array();

		$row = $this->get_db_row_from_order( $order, $this->order_column_mapping, $only_changes );
		if ( 'create' === $context && ! $row ) {
			throw new \Exception( 'No data for new record.' ); // This shouldn't occur.
		}

		if ( $row ) {
			$result[] = array(
				'table'  => self::get_orders_table_name(),
				'data'   => array_merge( $row['data'], array( 'id' => $order->get_id() ) ),
				'format' => array_merge( $row['format'], array( 'id' => '%d' ) ),
			);
		}

		// wc_order_operational_data.
		$row = $this->get_db_row_from_order( $order, $this->operational_data_column_mapping, $only_changes );
		if ( $row ) {
			$result[] = array(
				'table'  => self::get_operational_data_table_name(),
				'data'   => array_merge( $row['data'], array( 'order_id' => $order->get_id() ) ),
				'format' => array_merge( $row['format'], array( 'order_id' => '%d' ) ),
			);
		}

		// wc_order_addresses.
		foreach ( array( 'billing', 'shipping' ) as $address_type ) {
			$row = $this->get_db_row_from_order( $order, $this->{$address_type . '_address_column_mapping'}, $only_changes );

			if ( $row ) {
				$result[] = array(
					'table'  => self::get_addresses_table_name(),
					'data'   => array_merge(
						$row['data'],
						array(
							'order_id'     => $order->get_id(),
							'address_type' => $address_type,
						)
					),
					'format' => array_merge(
						$row['format'],
						array(
							'order_id'     => '%d',
							'address_type' => '%s',
						)
					),
				);
			}
		}

		/**
		 * Allow third parties to include rows that need to be inserted/updated in custom tables when persisting an order.
		 *
		 * @since 6.8.0
		 *
		 * @param array      Array of rows to be inserted/updated when persisting an order. Each entry should be an array with
		 *                   keys 'table', 'data' (the row), 'format' (row format), 'where' and 'where_format'.
		 * @param \WC_Order  The order object.
		 * @param string     The context of the operation: 'create' or 'update'.
		 */
		$ext_rows = apply_filters( 'woocommerce_orders_table_datastore_extra_db_rows_for_order', array(), $order, $context );

		return array_merge( $result, $ext_rows );
	}

	/**
	 * Produces an array with keys 'row' and 'format' that can be passed to `$wpdb->update()` as the `$data` and
	 * `$format` parameters. Values are taken from the order changes array and properly formatted for inclusion in the
	 * database.
	 *
	 * @param \WC_Abstract_Order $order          Order.
	 * @param array              $column_mapping Table column mapping.
	 * @param bool               $only_changes   Whether to consider only changes in the order object or all fields.
	 * @return array
	 *
	 * @since 6.8.0
	 */
	protected function get_db_row_from_order( $order, $column_mapping, $only_changes = false ) {
		$changes = $only_changes ? $order->get_changes() : array_merge( $order->get_data(), $order->get_changes() );

		$changes['type'] = $order->get_type();

		// Make sure 'status' is correct.
		if ( array_key_exists( 'status', $column_mapping ) ) {
			$changes['status'] = $this->get_post_status( $order );
		}

		$row        = array();
		$row_format = array();

		foreach ( $column_mapping as $column => $details ) {
			if ( ! isset( $details['name'] ) || ! array_key_exists( $details['name'], $changes ) ) {
				continue;
			}

			$row[ $column ]        = $this->database_util->format_object_value_for_db( $changes[ $details['name'] ], $details['type'] );
			$row_format[ $column ] = $this->database_util->get_wpdb_format_for_type( $details['type'] );
		}

		if ( ! $row ) {
			return false;
		}

		return array(
			'data'   => $row,
			'format' => $row_format,
		);
	}


	//phpcs:disable Squiz.Commenting, Generic.Commenting

	/**
	 * Method to delete an order from the database.
	 *
	 * @param \WC_Abstract_Order $order Order object.
	 * @param array              $args Array of args to pass to the delete method.
	 *
	 * @return void
	 */
	public function delete( &$order, $args = array() ) {
		$order_id = $order->get_id();

		if ( ! $order_id ) {
			return;
		}

		if ( ! empty( $args['force_delete'] ) ) {

			/**
			 * Fires immediately before an order is deleted from the database.
			 *
			 * @since 7.1.0
			 *
			 * @param int      $order_id ID of the order about to be deleted.
			 * @param WC_Order $order    Instance of the order that is about to be deleted.
			 */
			do_action( 'woocommerce_before_delete_order', $order_id, $order );

			$this->delete_order_data_from_custom_order_tables( $order_id );
			$order->set_id( 0 );

			// If this datastore method is called while the posts table is authoritative, refrain from deleting post data.
			if ( ! is_a( $order->get_data_store(), self::class ) ) {
				return;
			}

			// Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}.
			// Once we stop creating posts for orders, we should do the cleanup here instead.
			wp_delete_post( $order_id );

			do_action( 'woocommerce_delete_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
		} else {
			/**
			 * Fires immediately before an order is trashed.
			 *
			 * @since 7.1.0
			 *
			 * @param int      $order_id ID of the order about to be deleted.
			 * @param WC_Order $order    Instance of the order that is about to be deleted.
			 */
			do_action( 'woocommerce_before_trash_order', $order_id, $order );

			$this->trash_order( $order );

			do_action( 'woocommerce_trash_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
		}
	}

	/**
	 * Trashes an order.
	 *
	 * @param WC_Order $order The order object
	 * @return void
	 */
	public function trash_order( $order ) {
		global $wpdb;

		if ( 'trash' === $order->get_status( 'edit' ) ) {
			return;
		}

		$trash_metadata = array(
			'_wp_trash_meta_status' => 'wc-' . $order->get_status( 'edit' ),
			'_wp_trash_meta_time'   => time(),
		);

		foreach ( $trash_metadata as $meta_key => $meta_value ) {
			$this->add_meta(
				$order,
				(object) array(
					'key'   => $meta_key,
					'value' => $meta_value,
				)
			);
		}

		$wpdb->update(
			self::get_orders_table_name(),
			array(
				'status'           => 'trash',
				'date_updated_gmt' => current_time( 'Y-m-d H:i:s', true ),
			),
			array( 'id' => $order->get_id() ),
			array( '%s', '%s' ),
			array( '%d' )
		);

		$order->set_status( 'trash' );

		$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
		if ( $data_synchronizer->data_sync_is_enabled() ) {
			wp_trash_post( $order->get_id() );
		}
	}

	/**
	 * Attempts to restore the specified order back to its original status (after having been trashed).
	 *
	 * @param WC_Order $order The order to be untrashed.
	 *
	 * @return bool If the operation was successful.
	 */
	public function untrash_order( WC_Order $order ): bool {
		$id     = $order->get_id();
		$status = $order->get_status();

		if ( 'trash' !== $status ) {
			wc_get_logger()->warning(
				sprintf(
					/* translators: 1: order ID, 2: order status */
					__( 'Order %1$d cannot be restored from the trash: it has already been restored to status "%2$s".', 'woocommerce' ),
					$id,
					$status
				)
			);
			return false;
		}

		$previous_status           = $order->get_meta( '_wp_trash_meta_status' );
		$valid_statuses            = wc_get_order_statuses();
		$previous_state_is_invalid = ! array_key_exists( $previous_status, $valid_statuses );
		$pending_is_valid_status   = array_key_exists( 'wc-pending', $valid_statuses );

		if ( $previous_state_is_invalid && $pending_is_valid_status ) {
			// If the previous status is no longer valid, let's try to restore it to "pending" instead.
			wc_get_logger()->warning(
				sprintf(
					/* translators: 1: order ID, 2: order status */
					__( 'The previous status of order %1$d ("%2$s") is invalid. It has been restored to "pending" status instead.', 'woocommerce' ),
					$id,
					$previous_status
				)
			);

			$previous_status = 'pending';
		} elseif ( $previous_state_is_invalid ) {
			// If we cannot restore to pending, we should probably stand back and let the merchant intervene some other way.
			wc_get_logger()->warning(
				sprintf(
					/* translators: 1: order ID, 2: order status */
					__( 'The previous status of order %1$d ("%2$s") is invalid. It could not be restored.', 'woocommerce' ),
					$id,
					$previous_status
				)
			);

			return false;
		}

		/**
		 * Fires before an order is restored from the trash.
		 *
		 * @since 7.2.0
		 *
		 * @param int    $order_id        Order ID.
		 * @param string $previous_status The status of the order before it was trashed.
		 */
		do_action( 'woocommerce_untrash_order', $order->get_id(), $previous_status );

		$order->set_status( $previous_status );
		$order->save();

		// Was the status successfully restored? Let's clean up the meta and indicate success...
		if ( 'wc-' . $order->get_status() === $previous_status ) {
			$order->delete_meta_data( '_wp_trash_meta_status' );
			$order->delete_meta_data( '_wp_trash_meta_time' );
			$order->delete_meta_data( '_wp_trash_meta_comments_status' );
			$order->save_meta_data();

			$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
			if ( $data_synchronizer->data_sync_is_enabled() ) {
				//The previous $order->save() will have forced a sync to the posts table,
				//this implies that the post status is not "trash" anymore, and thus
				//wp_untrash_post would do nothing.
				wp_update_post(
					array(
						'ID'          => $id,
						'post_status' => 'trash',
					)
				);

				wp_untrash_post( $id );
			}

			return true;
		}

		// ...Or log a warning and bail.
		wc_get_logger()->warning(
			sprintf(
				/* translators: 1: order ID, 2: order status */
				__( 'Something went wrong when trying to restore order %d from the trash. It could not be restored.', 'woocommerce' ),
				$id
			)
		);

		return false;
	}


	/**
	 * Deletes order data from custom order tables.
	 *
	 * @param int $order_id The order ID.
	 * @return void
	 */
	public function delete_order_data_from_custom_order_tables( $order_id ) {
		global $wpdb;

		// Delete COT-specific data.
		foreach ( $this->get_all_table_names() as $table ) {
			$wpdb->delete(
				$table,
				( self::get_orders_table_name() === $table )
					? array( 'id' => $order_id )
					: array( 'order_id' => $order_id ),
				array( '%d' )
			);
		}
	}

	/**
	 * Method to create an order in the database.
	 *
	 * @param \WC_Order $order Order object.
	 */
	public function create( &$order ) {
		if ( '' === $order->get_order_key() ) {
			$order->set_order_key( wc_generate_order_key() );
		}

		$this->persist_save( $order );

		/**
		 * Fires when a new order is created.
		 *
		 * @since 2.7.0
		 *
		 * @param int       Order ID.
		 * @param \WC_Order Order object.
		 */
		do_action( 'woocommerce_new_order', $order->get_id(), $order );
	}

	/**
	 * Helper method responsible for persisting new data to order table.
	 *
	 * This should not contain and specific meta or actions, so that it can be used other order types safely.
	 *
	 * @param \WC_Order $order Order object.
	 *
	 * @return void
	 *
	 * @throws \Exception When unable to save data.
	 */
	protected function persist_save( &$order, bool $force_all_fields = false, $backfill = true ) {
		$order->set_version( Constants::get_constant( 'WC_VERSION' ) );
		$order->set_currency( $order->get_currency() ? $order->get_currency() : get_woocommerce_currency() );

		if ( ! $order->get_date_created( 'edit' ) ) {
			$order->set_date_created( time() );
		}

		$this->update_order_meta( $order );

		$this->persist_order_to_db( $order, $force_all_fields );

		$order->save_meta_data();
		$order->apply_changes();

		if ( $backfill ) {
			$this->maybe_backfill_post_record( $order );
		}
		$this->clear_caches( $order );
	}

	/**
	 * Method to update an order in the database.
	 *
	 * @param \WC_Order $order
	 */
	public function update( &$order ) {
		// Before updating, ensure date paid is set if missing.
		if (
			! $order->get_date_paid( 'edit' )
			&& version_compare( $order->get_version( 'edit' ), '3.0', '<' )
			&& $order->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order ) ) // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
		) {
			$order->set_date_paid( $order->get_date_created( 'edit' ) );
		}

		if ( null === $order->get_date_created( 'edit' ) ) {
			$order->set_date_created( time() );
		}

		$order->set_version( Constants::get_constant( 'WC_VERSION' ) );

		// Fetch changes.
		$changes = $order->get_changes();
		$this->persist_updates( $order );

		// Update download permissions if necessary.
		if ( array_key_exists( 'billing_email', $changes ) || array_key_exists( 'customer_id', $changes ) ) {
			$data_store = \WC_Data_Store::load( 'customer-download' );
			$data_store->update_user_by_order_id( $order->get_id(), $order->get_customer_id(), $order->get_billing_email() );
		}

		// Mark user account as active.
		if ( array_key_exists( 'customer_id', $changes ) ) {
			wc_update_user_last_active( $order->get_customer_id() );
		}

		$order->apply_changes();
		$this->clear_caches( $order );

		do_action( 'woocommerce_update_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
	}

	/**
	 * Proxy to udpating order meta. Here for backward compatibility reasons.
	 *
	 * @param \WC_Order $order Order object.
	 *
	 * @return void
	 */
	protected function update_post_meta( &$order ) {
		$this->update_order_meta( $order );
	}

	/**
	 * Helper method that is responsible for persisting order updates to the database.
	 *
	 * This is expected to be reused by other order types, and should not contain any specific metadata updates or actions.
	 *
	 * @param \WC_Order $order Order object.
	 *
	 * @return array $changes Array of changes.
	 *
	 * @throws \Exception When unable to persist order.
	 */
	protected function persist_updates( &$order, $backfill = true ) {
		// Fetch changes.
		$changes = $order->get_changes();

		if ( ! isset( $changes['date_modified'] ) ) {
			$order->set_date_modified( time() );
		}

		if ( $backfill ) {
			$this->maybe_backfill_post_record( $order );
		}

		$this->persist_order_to_db( $order );
		$order->save_meta_data();

		return $changes;
	}

	/**
	 * Helper function to decide whether to backfill post record.
	 *
	 * @param \WC_Abstract_Order $order Order object.
	 *
	 * @return void
	 */
	private function maybe_backfill_post_record( $order ) {
		$data_sync = wc_get_container()->get( DataSynchronizer::class );
		if ( $data_sync->data_sync_is_enabled() ) {
			$this->backfill_post_record( $order );
		}
	}

	/**
	 * Helper method that updates post meta based on an order object.
	 * Mostly used for backwards compatibility purposes in this datastore.
	 *
	 * @param \WC_Order $order Order object.
	 *
	 * @since 7.0.0
	 */
	public function update_order_meta( &$order ) {
		$changes = $order->get_changes();
		$this->update_address_index_meta( $order, $changes );
	}

	/**
	 * Helper function to update billing and shipping address metadata.
	 * @param \WC_Abstract_Order $order Order Object
	 * @param array              $changes Array of changes.
	 *
	 * @return void
	 */
	private function update_address_index_meta( $order, $changes ) {
		// If address changed, store concatenated version to make searches faster.
		foreach ( array( 'billing', 'shipping' ) as $address_type ) {
			if ( isset( $changes[ $address_type ] ) ) {
				$order->update_meta_data( "_{$address_type}_address_index", implode( ' ', $order->get_address( $address_type ) ) );
			}
		}
	}

	/**
	 * Return array of coupon_code => meta_key for coupon which have usage limit and have tentative keys.
	 * Pass $coupon_id if key for only one of the coupon is needed.
	 *
	 * @param WC_Order $order     Order object.
	 * @param int      $coupon_id If passed, will return held key for that coupon.
	 *
	 * @return array|string Key value pair for coupon code and meta key name. If $coupon_id is passed, returns meta_key for only that coupon.
	 */
	public function get_coupon_held_keys( $order, $coupon_id = null ) {
		$held_keys = $order->get_meta( '_coupon_held_keys' );
		if ( $coupon_id ) {
			return isset( $held_keys[ $coupon_id ] ) ? $held_keys[ $coupon_id ] : null;
		}
		return $held_keys;
	}

	/**
	 * Return array of coupon_code => meta_key for coupon which have usage limit per customer and have tentative keys.
	 *
	 * @param WC_Order $order Order object.
	 * @param int      $coupon_id If passed, will return held key for that coupon.
	 *
	 * @return mixed
	 */
	public function get_coupon_held_keys_for_users( $order, $coupon_id = null ) {
		$held_keys_for_user = $order->get_meta( '_coupon_held_keys_for_users' );
		if ( $coupon_id ) {
			return isset( $held_keys_for_user[ $coupon_id ] ) ? $held_keys_for_user[ $coupon_id ] : null;
		}
		return $held_keys_for_user;
	}

	/**
	 * Add/Update list of meta keys that are currently being used by this order to hold a coupon.
	 * This is used to figure out what all meta entries we should delete when order is cancelled/completed.
	 *
	 * @param WC_Order $order              Order object.
	 * @param array    $held_keys          Array of coupon_code => meta_key.
	 * @param array    $held_keys_for_user Array of coupon_code => meta_key for held coupon for user.
	 *
	 * @return mixed
	 */
	public function set_coupon_held_keys( $order, $held_keys, $held_keys_for_user ) {
		if ( is_array( $held_keys ) && 0 < count( $held_keys ) ) {
			$order->update_meta_data( '_coupon_held_keys', $held_keys );
		}
		if ( is_array( $held_keys_for_user ) && 0 < count( $held_keys_for_user ) ) {
			$order->update_meta_data( '_coupon_held_keys_for_users', $held_keys_for_user );
		}
	}

	/**
	 * Release all coupons held by this order.
	 *
	 * @param WC_Order $order Current order object.
	 * @param bool     $save  Whether to delete keys from DB right away. Could be useful to pass `false` if you are building a bulk request.
	 */
	public function release_held_coupons( $order, $save = true ) {
		$coupon_held_keys = $this->get_coupon_held_keys( $order );
		if ( is_array( $coupon_held_keys ) ) {
			foreach ( $coupon_held_keys as $coupon_id => $meta_key ) {
				$coupon = new \WC_Coupon( $coupon_id );
				$coupon->delete_meta_data( $meta_key );
				$coupon->save_meta_data();
			}
		}
		$order->delete_meta_data( '_coupon_held_keys' );

		$coupon_held_keys_for_users = $this->get_coupon_held_keys_for_users( $order );
		if ( is_array( $coupon_held_keys_for_users ) ) {
			foreach ( $coupon_held_keys_for_users as $coupon_id => $meta_key ) {
				$coupon = new \WC_Coupon( $coupon_id );
				$coupon->delete_meta_data( $meta_key );
				$coupon->save_meta_data();
			}
		}
		$order->delete_meta_data( '_coupon_held_keys_for_users' );

		if ( $save ) {
			$order->save_meta_data();
		}

	}

	public function query( $query_vars ) {
		if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) {
			$query_vars['no_found_rows'] = true;
		}

		if ( isset( $query_vars['anonymized'] ) ) {
			$query_vars['meta_query'] = $query_vars['meta_query'] ?? array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query

			if ( $query_vars['anonymized'] ) {
				$query_vars['meta_query'][] = array(
					'key'   => '_anonymized',
					'value' => 'yes',
				);
			} else {
				$query_vars['meta_query'][] = array(
					'key'     => '_anonymized',
					'compare' => 'NOT EXISTS',
				);
			}
		}

		try {
			$query = new OrdersTableQuery( $query_vars );
		} catch ( \Exception $e ) {
			$query = (object) array(
				'orders'        => array(),
				'found_orders'  => 0,
				'max_num_pages' => 0,
			);
		}

		if ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) {
			$orders = $query->orders;
		} else {
			$orders = WC()->order_factory->get_orders( $query->orders );
		}

		if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
			return (object) array(
				'orders'        => $orders,
				'total'         => $query->found_orders,
				'max_num_pages' => $query->max_num_pages,
			);
		}

		return $orders;
	}

	public function get_order_item_type( $order, $order_item_id ) {
		return 'line_item';
	}

	//phpcs:enable Squiz.Commenting, Generic.Commenting

	/**
	 * Get the SQL needed to create all the tables needed for the custom orders table feature.
	 *
	 * @return string
	 */
	public function get_database_schema() {
		global $wpdb;

		$collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : '';

		$orders_table_name           = $this->get_orders_table_name();
		$addresses_table_name        = $this->get_addresses_table_name();
		$operational_data_table_name = $this->get_operational_data_table_name();
		$meta_table                  = $this->get_meta_table_name();

		$sql = "
CREATE TABLE $orders_table_name (
	id bigint(20) unsigned,
	status varchar(20) null,
	currency varchar(10) null,
	type varchar(20) null,
	tax_amount decimal(26,8) null,
	total_amount decimal(26,8) null,
	customer_id bigint(20) unsigned null,
	billing_email varchar(320) null,
	date_created_gmt datetime null,
	date_updated_gmt datetime null,
	parent_order_id bigint(20) unsigned null,
	payment_method varchar(100) null,
	payment_method_title text null,
	transaction_id varchar(100) null,
	ip_address varchar(100) null,
	user_agent text null,
	customer_note text null,
	PRIMARY KEY (id),
	KEY status (status),
	KEY date_created (date_created_gmt),
	KEY customer_id_billing_email (customer_id, billing_email),
	KEY billing_email (billing_email),
	KEY type_status (type, status),
	KEY parent_order_id (parent_order_id),
	KEY date_updated (date_updated_gmt)
) $collate;
CREATE TABLE $addresses_table_name (
	id bigint(20) unsigned auto_increment primary key,
	order_id bigint(20) unsigned NOT NULL,
	address_type varchar(20) null,
	first_name text null,
	last_name text null,
	company text null,
	address_1 text null,
	address_2 text null,
	city text null,
	state text null,
	postcode text null,
	country text null,
	email varchar(320) null,
	phone varchar(100) null,
	KEY order_id (order_id),
	UNIQUE KEY address_type_order_id (address_type, order_id),
	KEY email (email),
	KEY phone (phone)
) $collate;
CREATE TABLE $operational_data_table_name (
	id bigint(20) unsigned auto_increment primary key,
	order_id bigint(20) unsigned NULL,
	created_via varchar(100) NULL,
	woocommerce_version varchar(20) NULL,
	prices_include_tax tinyint(1) NULL,
	coupon_usages_are_counted tinyint(1) NULL,
	download_permission_granted tinyint(1) NULL,
	cart_hash varchar(100) NULL,
	new_order_email_sent tinyint(1) NULL,
	order_key varchar(100) NULL,
	order_stock_reduced tinyint(1) NULL,
	date_paid_gmt datetime NULL,
	date_completed_gmt datetime NULL,
	shipping_tax_amount decimal(26, 8) NULL,
	shipping_total_amount decimal(26, 8) NULL,
	discount_tax_amount decimal(26, 8) NULL,
	discount_total_amount decimal(26, 8) NULL,
	recorded_sales tinyint(1) NULL,
	UNIQUE KEY order_id (order_id),
	UNIQUE KEY order_key (order_key)
) $collate;
CREATE TABLE $meta_table (
	id bigint(20) unsigned auto_increment primary key,
	order_id bigint(20) unsigned null,
	meta_key varchar(255),
	meta_value text null,
	KEY meta_key_value (meta_key, meta_value(100)),
	KEY order_id_meta_key_meta_value (order_id, meta_key, meta_value(100))
) $collate;
";

		return $sql;
	}

	/**
	 * Returns an array of meta for an object.
	 *
	 * @param  WC_Data $object WC_Data object.
	 * @return array
	 */
	public function read_meta( &$object ) {
		$raw_meta_data = $this->data_store_meta->read_meta( $object );
		return $this->filter_raw_meta_data( $object, $raw_meta_data );
	}

	/**
	 * Deletes meta based on meta ID.
	 *
	 * @param  WC_Data  $object WC_Data object.
	 * @param  stdClass $meta (containing at least ->id).
	 */
	public function delete_meta( &$object, $meta ) {
		return $this->data_store_meta->delete_meta( $object, $meta );
	}

	/**
	 * Add new piece of meta.
	 *
	 * @param  WC_Data  $object WC_Data object.
	 * @param  stdClass $meta (containing ->key and ->value).
	 * @return int meta ID
	 */
	public function add_meta( &$object, $meta ) {
		return $this->data_store_meta->add_meta( $object, $meta );
	}

	/**
	 * Update meta.
	 *
	 * @param  WC_Data  $object WC_Data object.
	 * @param  stdClass $meta (containing ->id, ->key and ->value).
	 */
	public function update_meta( &$object, $meta ) {
		return $this->data_store_meta->update_meta( $object, $meta );
	}
}
Internal/DataStores/Orders/OrdersTableRefundDataStore.php000064400000012445152333011040017517 0ustar00<?php
/**
 * Order refund data store. Refunds are based on orders (essentially negative orders) but there is slight difference in how we save them.
 * For example, order save hooks etc can't be fired when saving refund, so we need to do it a separate datastore.
 */

namespace Automattic\WooCommerce\Internal\DataStores\Orders;

/**
 * Class OrdersTableRefundDataStore.
 */
class OrdersTableRefundDataStore extends OrdersTableDataStore {

	/**
	 * We do not have and use all the getters and setters from OrderTableDataStore, so we only select the props we actually need.
	 *
	 * @var \string[][]
	 */
	protected $operational_data_column_mapping = array(
		'id'                        => array( 'type' => 'int' ),
		'order_id'                  => array( 'type' => 'int' ),
		'woocommerce_version'       => array(
			'type' => 'string',
			'name' => 'version',
		),
		'prices_include_tax'        => array(
			'type' => 'bool',
			'name' => 'prices_include_tax',
		),
		'coupon_usages_are_counted' => array(
			'type' => 'bool',
			'name' => 'recorded_coupon_usage_counts',
		),
		'shipping_tax_amount'       => array(
			'type' => 'decimal',
			'name' => 'shipping_tax',
		),
		'shipping_total_amount'     => array(
			'type' => 'decimal',
			'name' => 'shipping_total',
		),
		'discount_tax_amount'       => array(
			'type' => 'decimal',
			'name' => 'discount_tax',
		),
		'discount_total_amount'     => array(
			'type' => 'decimal',
			'name' => 'discount_total',
		),
	);

	/**
	 * Delete a refund order from database.
	 *
	 * @param \WC_Order $refund Refund object to delete.
	 * @param array     $args Array of args to pass to the delete method.
	 *
	 * @return void
	 */
	public function delete( &$refund, $args = array() ) {
		$refund_id = $refund->get_id();
		if ( ! $refund_id ) {
			return;
		}

		$this->delete_order_data_from_custom_order_tables( $refund_id );
		$refund->set_id( 0 );

		// If this datastore method is called while the posts table is authoritative, refrain from deleting post data.
		if ( ! is_a( $refund->get_data_store(), self::class ) ) {
			return;
		}

		// Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}.
		// Once we stop creating posts for orders, we should do the cleanup here instead.
		wp_delete_post( $refund_id );
	}

	/**
	 * Read a refund object from custom tables.
	 *
	 * @param \WC_Abstract_Order $refund Refund object.
	 *
	 * @return void
	 */
	public function read( &$refund ) {
		parent::read( $refund );
		$this->set_refund_props( $refund );
	}

	/**
	 * Read multiple refund objects from custom tables.
	 *
	 * @param \WC_Order $refunds Refund objects.
	 */
	public function read_multiple( &$refunds ) {
		parent::read_multiple( $refunds );
		foreach ( $refunds as $refund ) {
			$this->set_refund_props( $refund );
		}
	}

	/**
	 * Helper method to set refund props.
	 *
	 * @param \WC_Order $refund Refund object.
	 */
	private function set_refund_props( $refund ) {
		$refund->set_props(
			array(
				'amount'           => $refund->get_meta( '_refund_amount', true ),
				'refunded_by'      => $refund->get_meta( '_refunded_by', true ),
				'refunded_payment' => wc_string_to_bool( $refund->get_meta( '_refunded_payment', true ) ),
				'reason'           => $refund->get_meta( '_refund_reason', true ),
			)
		);
	}

	/**
	 * Method to create a refund in the database.
	 *
	 * @param \WC_Abstract_Order $refund Refund object.
	 */
	public function create( &$refund ) {
		$refund->set_status( 'completed' ); // Refund are always marked completed.
		$this->persist_save( $refund );
	}

	/**
	 * Update refund in database.
	 *
	 * @param \WC_Order $refund Refund object.
	 */
	public function update( &$refund ) {
		$this->persist_updates( $refund );
	}

	/**
	 * Helper method that updates post meta based on an refund object.
	 * Mostly used for backwards compatibility purposes in this datastore.
	 *
	 * @param \WC_Order $refund Refund object.
	 */
	public function update_order_meta( &$refund ) {
		parent::update_order_meta( $refund );

		// Update additional props.
		$updated_props     = array();
		$meta_key_to_props = array(
			'_refund_amount'    => 'amount',
			'_refunded_by'      => 'refunded_by',
			'_refunded_payment' => 'refunded_payment',
			'_refund_reason'    => 'reason',
		);

		$props_to_update = $this->get_props_to_update( $refund, $meta_key_to_props );
		foreach ( $props_to_update as $meta_key => $prop ) {
			$value = $refund->{"get_$prop"}( 'edit' );
			$refund->update_meta_data( $meta_key, $value );
			$updated_props[] = $prop;
		}

		/**
		 * Fires after updating meta for a order refund.
		 *
		 * @since 2.7.0
		 */
		do_action( 'woocommerce_order_refund_object_updated_props', $refund, $updated_props );
	}

	/**
	 * Get a title for the new post type.
	 *
	 * @return string
	 */
	protected function get_post_title() {
		return sprintf(
		/* translators: %s: Order date */
			__( 'Refund &ndash; %s', 'woocommerce' ),
			( new \DateTime( 'now' ) )->format( _x( 'M d, Y @ h:i A', 'Order date parsed by DateTime::format', 'woocommerce' ) ) // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment, WordPress.WP.I18n.UnorderedPlaceholdersText
		);
	}


	/**
	 * Returns data store object to use backfilling.
	 *
	 * @return \WC_Order_Refund_Data_Store_CPT
	 */
	protected function get_post_data_store_for_backfill() {
		return new \WC_Order_Refund_Data_Store_CPT();
	}

}
Internal/DataStores/Orders/OrdersTableFieldQuery.php000064400000020730152333011040016532 0ustar00<?php
namespace Automattic\WooCommerce\Internal\DataStores\Orders;

defined( 'ABSPATH' ) || exit;

/**
 * Provides the implementation for `field_query` in {@see OrdersTableQuery} used to build
 * complex queries against order fields in the database.
 *
 * @internal
 */
class OrdersTableFieldQuery {

	/**
	 * List of valid SQL operators to use as field_query 'compare' values.
	 *
	 * @var array
	 */
	private const VALID_COMPARISON_OPERATORS = array(
		'=',
		'!=',
		'LIKE',
		'NOT LIKE',
		'IN',
		'NOT IN',
		'EXISTS',
		'NOT EXISTS',
		'RLIKE',
		'REGEXP',
		'NOT REGEXP',
		'>',
		'>=',
		'<',
		'<=',
		'BETWEEN',
		'NOT BETWEEN',
	);

	/**
	 * The original query object.
	 *
	 * @var OrdersTableQuery
	 */
	private $query = null;

	/**
	 * Determines whether the field query should produce no results due to an invalid argument.
	 *
	 * @var boolean
	 */
	private $force_no_results = false;

	/**
	 * Holds a sanitized version of the `field_query`.
	 *
	 * @var array
	 */
	private $queries = array();

	/**
	 * JOIN clauses to add to the main SQL query.
	 *
	 * @var array
	 */
	private $join = array();

	/**
	 * WHERE clauses to add to the main SQL query.
	 *
	 * @var array
	 */
	private $where = array();

	/**
	 * Table aliases in use by the field query. Used to keep track of JOINs and optimize when possible.
	 *
	 * @var array
	 */
	private $table_aliases = array();


	/**
	 * Constructor.
	 *
	 * @param OrdersTableQuery $q The main query being performed.
	 */
	public function __construct( OrdersTableQuery $q ) {
		$field_query = $q->get( 'field_query' );

		if ( ! $field_query || ! is_array( $field_query ) ) {
			return;
		}

		$this->query   = $q;
		$this->queries = $this->sanitize_query( $field_query );
		$this->where   = ( ! $this->force_no_results ) ? $this->process( $this->queries ) : '1=0';
	}

	/**
	 * Sanitizes the field_query argument.
	 *
	 * @param array $q A field_query array.
	 * @return array A sanitized field query array.
	 * @throws \Exception When field table info is missing.
	 */
	private function sanitize_query( array $q ) {
		$sanitized = array();

		foreach ( $q as $key => $arg ) {
			if ( 'relation' === $key ) {
				$relation = $arg;
			} elseif ( ! is_array( $arg ) ) {
				continue;
			} elseif ( $this->is_atomic( $arg ) ) {
				if ( isset( $arg['value'] ) && array() === $arg['value'] ) {
					continue;
				}

				// Sanitize 'compare'.
				$arg['compare'] = strtoupper( $arg['compare'] ?? '=' );
				$arg['compare'] = in_array( $arg['compare'], self::VALID_COMPARISON_OPERATORS, true ) ? $arg['compare'] : '=';

				if ( '=' === $arg['compare'] && isset( $arg['value'] ) && is_array( $arg['value'] ) ) {
					$arg['compare'] = 'IN';
				}

				// Sanitize 'cast'.
				$arg['cast'] = $this->sanitize_cast_type( $arg['type'] ?? '' );

				$field_info = $this->query->get_field_mapping_info( $arg['field'] );
				if ( ! $field_info ) {
					$this->force_no_results = true;
					continue;
				}

				$arg = array_merge( $arg, $field_info );

				$sanitized[ $key ] = $arg;
			} else {
				$sanitized_arg = $this->sanitize_query( $arg );

				if ( $sanitized_arg ) {
					$sanitized[ $key ] = $sanitized_arg;
				}
			}
		}

		if ( $sanitized ) {
			$sanitized['relation'] = 1 === count( $sanitized ) ? 'OR' : $this->sanitize_relation( $relation ?? 'AND' );
		}

		return $sanitized;
	}

	/**
	 * Makes sure we use an AND or OR relation. Defaults to AND.
	 *
	 * @param string $relation An unsanitized relation prop.
	 * @return string
	 */
	private function sanitize_relation( string $relation ): string {
		if ( ! empty( $relation ) && 'OR' === strtoupper( $relation ) ) {
			return 'OR';
		}

		return 'AND';
	}

	/**
	 * Processes field_query entries and generates the necessary table aliases, JOIN statements and WHERE conditions.
	 *
	 * @param array $q A field query.
	 * @return string An SQL WHERE statement.
	 */
	private function process( array $q ) {
		$where = '';

		if ( empty( $q ) ) {
			return $where;
		}

		if ( $this->is_atomic( $q ) ) {
			$q['alias'] = $this->find_or_create_table_alias_for_clause( $q );
			$where      = $this->generate_where_for_clause( $q );
		} else {
			$relation = $q['relation'];
			unset( $q['relation'] );

			foreach ( $q as $query ) {
				$chunks[] = $this->process( $query );
			}

			if ( 1 === count( $chunks ) ) {
				$where = $chunks[0];
			} else {
				$where = '(' . implode( " {$relation} ", $chunks ) . ')';
			}
		}

		return $where;
	}

	/**
	 * Checks whether a given field_query clause is atomic or not (i.e. not nested).
	 *
	 * @param array $q The field_query clause.
	 * @return boolean TRUE if atomic, FALSE otherwise.
	 */
	private function is_atomic( $q ) {
		return isset( $q['field'] );
	}

	/**
	 * Finds a common table alias that the field_query clause can use, or creates one.
	 *
	 * @param array $q       An atomic field_query clause.
	 * @return string A table alias for use in an SQL JOIN clause.
	 * @throws \Exception When table info for clause is missing.
	 */
	private function find_or_create_table_alias_for_clause( $q ) {
		global $wpdb;

		if ( ! empty( $q['alias'] ) ) {
			return $q['alias'];
		}

		if ( empty( $q['table'] ) || empty( $q['column'] ) ) {
			throw new \Exception( __( 'Missing table info for query arg.', 'woocommerce' ) );
		}

		$join = '';

		if ( isset( $q['mapping_id'] ) ) {
			// Re-use JOINs and aliases from OrdersTableQuery for core tables.
			$alias = $this->query->get_core_mapping_alias( $q['mapping_id'] );
			$join  = $this->query->get_core_mapping_join( $q['mapping_id'] );
		} else {
			$alias = $q['table'];
			$join  = '';
		}

		if ( in_array( $alias, $this->table_aliases, true ) ) {
			return $alias;
		}

		$this->table_aliases[] = $alias;

		if ( $join ) {
			$this->join[] = $join;
		}

		return $alias;
	}

	/**
	 * Returns the correct type for a given clause 'type'.
	 *
	 * @param string $type MySQL type.
	 * @return string MySQL type.
	 */
	private function sanitize_cast_type( $type ) {
		$clause_type = strtoupper( $type );

		if ( ! $clause_type || ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $clause_type ) ) {
			return 'CHAR';
		}

		if ( 'NUMERIC' === $clause_type ) {
			$clause_type = 'SIGNED';
		}

		return $clause_type;
	}

	/**
	 * Generates an SQL WHERE clause for a given field_query atomic clause.
	 *
	 * @param array $clause An atomic field_query clause.
	 * @return string An SQL WHERE clause or an empty string if $clause is invalid.
	 */
	private function generate_where_for_clause( $clause ): string {
		global $wpdb;

		$clause_value = $clause['value'] ?? '';

		if ( in_array( $clause['compare'], array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) {
			if ( ! is_array( $clause_value ) ) {
				$clause_value = preg_split( '/[,\s]+/', $clause_value );
			}
		} elseif ( is_string( $clause_value ) ) {
			$clause_value = trim( $clause_value );
		}

		$clause_compare = $clause['compare'];

		switch ( $clause_compare ) {
			case 'IN':
			case 'NOT IN':
				$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( $clause_value ) ), 1 ) . ')', $clause_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				break;
			case 'BETWEEN':
			case 'NOT BETWEEN':
				$where = $wpdb->prepare( '%s AND %s', $clause_value[0], $clause_value[1] ?? $clause_value[0] );
				break;
			case 'LIKE':
			case 'NOT LIKE':
				$where = $wpdb->prepare( '%s', '%' . $wpdb->esc_like( $clause_value ) . '%' );
				break;
			case 'EXISTS':
				// EXISTS with a value is interpreted as '='.
				if ( $clause_value ) {
					$clause_compare = '=';
					$where          = $wpdb->prepare( '%s', $clause_value );
				} else {
					$clause_compare = 'IS NOT';
					$where          = 'NULL';
				}

				break;
			case 'NOT EXISTS':
				// 'value' is ignored for NOT EXISTS.
				$clause_compare = 'IS';
				$where          = 'NULL';
				break;
			default:
				$where = $wpdb->prepare( '%s', $clause_value );
				break;
		}

		if ( $where ) {
			if ( 'CHAR' === $clause['cast'] ) {
				return "`{$clause['alias']}`.`{$clause['column']}` {$clause_compare} {$where}";
			} else {
				return "CAST(`{$clause['alias']}`.`{$clause['column']}` AS {$clause['cast']}) {$clause_compare} {$where}";
			}
		}

		return '';
	}

	/**
	 * Returns JOIN and WHERE clauses to be appended to the main SQL query.
	 *
	 * @return array {
	 *     @type string $join  JOIN clause.
	 *     @type string $where WHERE clause.
	 * }
	 */
	public function get_sql_clauses() {
		return array(
			'join'  => $this->join,
			'where' => $this->where ? array( $this->where ) : array(),
		);
	}

}
Internal/DataStores/Orders/OrdersTableMetaQuery.php000064400000040553152333011040016402 0ustar00<?php
namespace Automattic\WooCommerce\Internal\DataStores\Orders;

defined( 'ABSPATH' ) || exit;

/**
 * Class used to implement meta queries for the orders table datastore via {@see OrdersTableQuery}.
 * Heavily inspired by WordPress' own `WP_Meta_Query` for backwards compatibility reasons.
 *
 * Parts of the implementation have been adapted from {@link https://core.trac.wordpress.org/browser/tags/6.0.1/src/wp-includes/class-wp-meta-query.php}.
 */
class OrdersTableMetaQuery {

	/**
	 * List of non-numeric SQL operators used for comparisons in meta queries.
	 *
	 * @var array
	 */
	private const NON_NUMERIC_OPERATORS = array(
		'=',
		'!=',
		'LIKE',
		'NOT LIKE',
		'IN',
		'NOT IN',
		'EXISTS',
		'NOT EXISTS',
		'RLIKE',
		'REGEXP',
		'NOT REGEXP',
	);

	/**
	 * List of numeric SQL operators used for comparisons in meta queries.
	 *
	 * @var array
	 */
	private const NUMERIC_OPERATORS = array(
		'>',
		'>=',
		'<',
		'<=',
		'BETWEEN',
		'NOT BETWEEN',

	);

	/**
	 * Prefix used when generating aliases for the metadata table.
	 *
	 * @var string
	 */
	private const ALIAS_PREFIX = 'meta';

	/**
	 * Name of the main orders table.
	 *
	 * @var string
	 */
	private $meta_table = '';

	/**
	 * Name of the metadata table.
	 *
	 * @var string
	 */
	private $orders_table = '';

	/**
	 * Sanitized `meta_query`.
	 *
	 * @var array
	 */
	private $queries = array();

	/**
	 * JOIN clauses to add to the main SQL query.
	 *
	 * @var array
	 */
	private $join = array();

	/**
	 * WHERE clauses to add to the main SQL query.
	 *
	 * @var array
	 */
	private $where = array();

	/**
	 * Table aliases in use by the meta query. Used to optimize JOINs when possible.
	 *
	 * @var array
	 */
	private $table_aliases = array();

	/**
	 * Constructor.
	 *
	 * @param OrdersTableQuery $q The main query being performed.
	 */
	public function __construct( OrdersTableQuery $q ) {
		$meta_query = $q->get( 'meta_query' );

		if ( ! $meta_query ) {
			return;
		}

		$this->queries = $this->sanitize_meta_query( $meta_query );

		$this->meta_table   = $q->get_table_name( 'meta' );
		$this->orders_table = $q->get_table_name( 'orders' );

		$this->build_query();
	}

	/**
	 * Returns JOIN and WHERE clauses to be appended to the main SQL query.
	 *
	 * @return array {
	 *     @type string $join  JOIN clause.
	 *     @type string $where WHERE clause.
	 * }
	 */
	public function get_sql_clauses(): array {
		return array(
			'join'  => $this->sanitize_join( $this->join ),
			'where' => $this->flatten_where_clauses( $this->where ),
		);
	}

	/**
	 * Checks whether a given meta_query clause is atomic or not (i.e. not nested).
	 *
	 * @param array $arg The meta_query clause.
	 * @return boolean TRUE if atomic, FALSE otherwise.
	 */
	private function is_atomic( array $arg ): bool {
		return isset( $arg['key'] ) || isset( $arg['value'] );
	}

	/**
	 * Sanitizes the meta_query argument.
	 *
	 * @param array $q A meta_query array.
	 * @return array A sanitized meta query array.
	 */
	private function sanitize_meta_query( array $q ): array {
		$sanitized = array();

		foreach ( $q as $key => $arg ) {
			if ( 'relation' === $key ) {
				$relation = $arg;
			} elseif ( ! is_array( $arg ) ) {
				continue;
			} elseif ( $this->is_atomic( $arg ) ) {
				if ( isset( $arg['value'] ) && array() === $arg['value'] ) {
					unset( $arg['value'] );
				}

				$arg['compare']      = isset( $arg['compare'] ) ? strtoupper( $arg['compare'] ) : ( isset( $arg['value'] ) && is_array( $arg['value'] ) ? 'IN' : '=' );
				$arg['compare_key']  = isset( $arg['compare_key'] ) ? strtoupper( $arg['compare_key'] ) : ( isset( $arg['key'] ) && is_array( $arg['key'] ) ? 'IN' : '=' );

				if ( ! in_array( $arg['compare'], self::NON_NUMERIC_OPERATORS, true ) && ! in_array( $arg['compare'], self::NUMERIC_OPERATORS, true ) ) {
					$arg['compare'] = '=';
				}

				if ( ! in_array( $arg['compare_key'], self::NON_NUMERIC_OPERATORS, true ) ) {
					$arg['compare_key'] = '=';
				}

				$sanitized[ $key ] = $arg;
			} else {
				$sanitized_arg = $this->sanitize_meta_query( $arg );

				if ( $sanitized_arg ) {
					$sanitized[ $key ] = $sanitized_arg;
				}
			}
		}

		if ( $sanitized ) {
			$sanitized['relation'] = 1 === count( $sanitized ) ? 'OR' : $this->sanitize_relation( $relation ?? 'AND' );
		}

		return $sanitized;
	}

	/**
	 * Makes sure we use an AND or OR relation. Defaults to AND.
	 *
	 * @param string $relation An unsanitized relation prop.
	 * @return string
	 */
	private function sanitize_relation( string $relation ): string {
		if ( ! empty( $relation ) && 'OR' === strtoupper( $relation ) ) {
			return 'OR';
		}

		return 'AND';
	}

	/**
	 * Returns the correct type for a given meta type.
	 *
	 * @param string $type MySQL type.
	 * @return string MySQL type.
	 */
	private function sanitize_cast_type( string $type = '' ): string {
		$meta_type = strtoupper( $type );

		if ( ! $meta_type || ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) ) {
			return 'CHAR';
		}

		if ( 'NUMERIC' === $meta_type ) {
			$meta_type = 'SIGNED';
		}

		return $meta_type;
	}

	/**
	 * Makes sure a JOIN array does not have duplicates.
	 *
	 * @param array $join A JOIN array.
	 * @return array A sanitized JOIN array.
	 */
	private function sanitize_join( array $join ): array {
		return array_filter( array_unique( array_map( 'trim', $join ) ) );
	}

	/**
	 * Flattens a nested WHERE array.
	 *
	 * @param array $where A possibly nested WHERE array with AND/OR operators.
	 * @return string An SQL WHERE clause.
	 */
	private function flatten_where_clauses( $where ): string {
		if ( is_string( $where ) ) {
			return trim( $where );
		}

		$chunks   = array();
		$operator = $this->sanitize_relation( $where['operator'] ?? '' );

		foreach ( $where as $key => $w ) {
			if ( 'operator' === $key ) {
				continue;
			}

			$flattened = $this->flatten_where_clauses( $w );
			if ( $flattened ) {
				$chunks[] = $flattened;
			}
		}

		if ( $chunks ) {
			return '(' . implode( " {$operator} ", $chunks ) . ')';
		} else {
			return '';
		}
	}

	/**
	 * Builds all the required internal bits for this meta query.
	 *
	 * @return void
	 */
	private function build_query(): void {
		if ( ! $this->queries ) {
			return;
		}

		$queries     = $this->queries;
		$sql_where   = $this->process( $queries );
		$this->where = $sql_where;

	}

	/**
	 * Processes meta_query entries and generates the necessary table aliases, JOIN statements and WHERE conditions.
	 *
	 * @param array      $arg    A meta query.
	 * @param null|array $parent The parent of the element being processed.
	 * @return array A nested array of WHERE conditions.
	 */
	private function process( array &$arg, &$parent = null ): array {
		$where = array();

		if ( $this->is_atomic( $arg ) ) {
			$arg['alias'] = $this->find_or_create_table_alias_for_clause( $arg, $parent );
			$arg['cast']  = $this->sanitize_cast_type( $arg['type'] ?? '' );

			$where = array_filter(
				array(
					$this->generate_where_for_clause_key( $arg ),
					$this->generate_where_for_clause_value( $arg ),
				)
			);
		} else {
			// Nested.
			$relation = $arg['relation'];
			unset( $arg['relation'] );

			foreach ( $arg as $key => &$clause ) {
				$chunks[] = $this->process( $clause, $arg );
			}

			// Merge chunks of the form OR(m) with the surrounding clause.
			if ( 1 === count( $chunks ) ) {
				$where = $chunks[0];
			} else {
				$where = array_merge(
					array(
						'operator' => $relation,
					),
					$chunks
				);
			}
		}

		return $where;
	}

	/**
	 * Generates a JOIN clause to handle an atomic meta_query clause.
	 *
	 * @param array  $clause An atomic meta_query clause.
	 * @param string $alias  Metadata table alias to use.
	 * @return string An SQL JOIN clause.
	 */
	private function generate_join_for_clause( array $clause, string $alias ): string {
		global $wpdb;

		if ( 'NOT EXISTS' === $clause['compare'] ) {
			if ( 'LIKE' === $clause['compare_key'] ) {
				return $wpdb->prepare(
					"LEFT JOIN {$this->meta_table} AS {$alias} ON ( {$this->orders_table}.id = {$alias}.order_id AND {$alias}.meta_key LIKE %s )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					'%' . $wpdb->esc_like( $clause['key'] ) . '%'
				);
			} else {
				return $wpdb->prepare(
					"LEFT JOIN {$this->meta_table} AS {$alias} ON ( {$this->orders_table}.id = {$alias}.order_id AND {$alias}.meta_key = %s )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					$clause['key']
				);
			}
		}

		return "INNER JOIN {$this->meta_table} AS {$alias} ON ( {$this->orders_table}.id = {$alias}.order_id )";
	}

	/**
	 * Finds a common table alias that the meta_query clause can use, or creates one.
	 *
	 * @param array $clause       An atomic meta_query clause.
	 * @param array $parent_query The parent query this clause is in.
	 * @return string A table alias for use in an SQL JOIN clause.
	 */
	private function find_or_create_table_alias_for_clause( array $clause, array $parent_query ): string {
		if ( ! empty( $clause['alias'] ) ) {
			return $clause['alias'];
		}

		$alias    = false;
		$siblings = array_filter(
			$parent_query,
			array( __CLASS__, 'is_atomic' )
		);

		foreach ( $siblings as $sibling ) {
			if ( empty( $sibling['alias'] ) ) {
				continue;
			}

			if ( $this->is_operator_compatible_with_shared_join( $clause, $sibling, $parent_query['relation'] ?? 'AND' ) ) {
				$alias = $sibling['alias'];
				break;
			}
		}

		if ( ! $alias ) {
			$alias                 = self::ALIAS_PREFIX . count( $this->table_aliases );
			$this->join[]          = $this->generate_join_for_clause( $clause, $alias );
			$this->table_aliases[] = $alias;
		}

		return $alias;
	}

	/**
	 * Checks whether two meta_query clauses can share a JOIN.
	 *
	 * @param array  $clause    An atomic meta_query clause.
	 * @param array  $sibling   An atomic meta_query clause.
	 * @param string $relation The relation involving both clauses.
	 * @return boolean TRUE if the clauses can share a table alias, FALSE otherwise.
	 */
	private function is_operator_compatible_with_shared_join( array $clause, array $sibling, string $relation = 'AND' ): bool {
		if ( ! $this->is_atomic( $clause ) || ! $this->is_atomic( $sibling ) ) {
			return false;
		}

		$valid_operators = array();

		if ( 'OR' === $relation ) {
			$valid_operators = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' );
		} elseif ( isset( $sibling['key'] ) && isset( $clause['key'] ) && $sibling['key'] === $clause['key'] ) {
			$valid_operators = array( '!=', 'NOT IN', 'NOT LIKE' );
		}

		return in_array( strtoupper( $clause['compare'] ), $valid_operators, true ) && in_array( strtoupper( $sibling['compare'] ), $valid_operators, true );
	}

	/**
	 * Generates an SQL WHERE clause for a given meta_query atomic clause based on its meta key.
	 * Adapted from WordPress' `WP_Meta_Query::get_sql_for_clause()` method.
	 *
	 * @param array $clause An atomic meta_query clause.
	 * @return string An SQL WHERE clause or an empty string if $clause is invalid.
	 */
	private function generate_where_for_clause_key( array $clause ): string {
		global $wpdb;

		if ( ! array_key_exists( 'key', $clause ) ) {
			return '';
		}

		if ( 'NOT EXISTS' === $clause['compare'] ) {
			return "{$clause['alias']}.order_id IS NULL";
		}

		$alias = $clause['alias'];

		if ( in_array( $clause['compare_key'], array( '!=', 'NOT IN', 'NOT LIKE', 'NOT EXISTS', 'NOT REGEXP' ), true ) ) {
			$i                     = count( $this->table_aliases );
			$subquery_alias        = self::ALIAS_PREFIX . $i;
			$this->table_aliases[] = $subquery_alias;

			$meta_compare_string_start  = 'NOT EXISTS (';
			$meta_compare_string_start .= "SELECT 1 FROM {$this->meta_table} {$subquery_alias} ";
			$meta_compare_string_start .= "WHERE {$subquery_alias}.order_id = {$alias}.order_id ";
			$meta_compare_string_end    = 'LIMIT 1';
			$meta_compare_string_end   .= ')';
		}

		switch ( $clause['compare_key'] ) {
			case '=':
			case 'EXISTS':
				$where = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				break;
			case 'LIKE':
				$meta_compare_value = '%' . $wpdb->esc_like( trim( $clause['key'] ) ) . '%';
				$where              = $wpdb->prepare( "$alias.meta_key LIKE %s", $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				break;
			case 'IN':
				$meta_compare_string = "$alias.meta_key IN (" . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ')';
				$where               = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				break;
			case 'RLIKE':
			case 'REGEXP':
				$operator = $clause['compare_key'];
				if ( isset( $clause['type_key'] ) && 'BINARY' === strtoupper( $clause['type_key'] ) ) {
					$cast = 'BINARY';
				} else {
					$cast = '';
				}
				$where = $wpdb->prepare( "$alias.meta_key $operator $cast %s", trim( $clause['key'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				break;
			case '!=':
			case 'NOT EXISTS':
				$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key = %s " . $meta_compare_string_end;
				$where               = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				break;
			case 'NOT LIKE':
				$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key LIKE %s " . $meta_compare_string_end;

				$meta_compare_value = '%' . $wpdb->esc_like( trim( $clause['key'] ) ) . '%';
				$where              = $wpdb->prepare( $meta_compare_string, $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				break;
			case 'NOT IN':
				$array_subclause     = '(' . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ') ';
				$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key IN " . $array_subclause . $meta_compare_string_end;
				$where               = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				break;
			case 'NOT REGEXP':
				$operator = $clause['compare_key'];
				if ( isset( $clause['type_key'] ) && 'BINARY' === strtoupper( $clause['type_key'] ) ) {
					$cast = 'BINARY';
				} else {
					$cast = '';
				}

				$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key REGEXP $cast %s " . $meta_compare_string_end;
				$where               = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				break;
			default:
				$where = '';
				break;
		}

		return $where;
	}

	/**
	 * Generates an SQL WHERE clause for a given meta_query atomic clause based on its meta value.
	 * Adapted from WordPress' `WP_Meta_Query::get_sql_for_clause()` method.
	 *
	 * @param array $clause An atomic meta_query clause.
	 * @return string An SQL WHERE clause or an empty string if $clause is invalid.
	 */
	private function generate_where_for_clause_value( $clause ): string {
		global $wpdb;

		if ( ! array_key_exists( 'value', $clause ) ) {
			return '';
		}

		$meta_value = $clause['value'];

		if ( in_array( $clause['compare'], array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) {
			if ( ! is_array( $meta_value ) ) {
				$meta_value = preg_split( '/[,\s]+/', $meta_value );
			}
		} elseif ( is_string( $meta_value ) ) {
			$meta_value = trim( $meta_value );
		}

		$meta_compare = $clause['compare'];

		switch ( $meta_compare ) {
			case 'IN':
			case 'NOT IN':
				$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')', $meta_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				break;

			case 'BETWEEN':
			case 'NOT BETWEEN':
				$where = $wpdb->prepare( '%s AND %s', $meta_value[0], $meta_value[1] );
				break;

			case 'LIKE':
			case 'NOT LIKE':
				$where = $wpdb->prepare( '%s', '%' . $wpdb->esc_like( $meta_value ) . '%' );
				break;

			// EXISTS with a value is interpreted as '='.
			case 'EXISTS':
				$meta_compare = '=';
				$where        = $wpdb->prepare( '%s', $meta_value );
				break;

			// 'value' is ignored for NOT EXISTS.
			case 'NOT EXISTS':
				$where = '';
				break;

			default:
				$where = $wpdb->prepare( '%s', $meta_value );
				break;
		}

		if ( $where ) {
			if ( 'CHAR' === $clause['cast'] ) {
				return "{$clause['alias']}.meta_value {$meta_compare} {$where}";
			} else {
				return "CAST({$clause['alias']}.meta_value AS {$clause['cast']}) {$meta_compare} {$where}";
			}
		}
	}

}
Internal/DataStores/CustomMetaDataStore.php000064400000011656152333011040014773 0ustar00<?php
/**
 * CustomMetaDataStore class file.
 */

namespace Automattic\WooCommerce\Internal\DataStores;

/**
 * Implements functions similar to WP's add_metadata(), get_metadata(), and friends using a custom table.
 *
 * @see WC_Data_Store_WP For an implementation using WP's metadata functions and tables.
 */
abstract class CustomMetaDataStore {

	/**
	 * Returns the name of the table used for storage.
	 *
	 * @return string
	 */
	abstract protected function get_table_name();

	/**
	 * Returns the name of the field/column used for identifiying metadata entries.
	 *
	 * @return string
	 */
	protected function get_meta_id_field() {
		return 'id';
	}

	/**
	 * Returns the name of the field/column used for associating meta with objects.
	 *
	 * @return string
	 */
	protected function get_object_id_field() {
		return 'object_id';
	}

	/**
	 * Describes the structure of the metadata table.
	 *
	 * @return array Array elements: table, object_id_field, meta_id_field.
	 */
	protected function get_db_info() {
		return array(
			'table'           => $this->get_table_name(),
			'meta_id_field'   => $this->get_meta_id_field(),
			'object_id_field' => $this->get_object_id_field(),
		);
	}

	/**
	 * Returns an array of meta for an object.
	 *
	 * @param  WC_Data $object WC_Data object.
	 * @return array
	 */
	public function read_meta( &$object ) {
		global $wpdb;

		$db_info = $this->get_db_info();

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$raw_meta_data = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT {$db_info['meta_id_field']} AS meta_id, meta_key, meta_value FROM {$db_info['table']} WHERE {$db_info['object_id_field']} = %d ORDER BY meta_id",
				$object->get_id()
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		return $raw_meta_data;
	}

	/**
	 * Deletes meta based on meta ID.
	 *
	 * @param  WC_Data  $object WC_Data object.
	 * @param  stdClass $meta (containing at least ->id).
	 */
	public function delete_meta( &$object, $meta ) {
		global $wpdb;

		if ( ! isset( $meta->id ) ) {
			return false;
		}

		$db_info = $this->get_db_info();
		$meta_id = absint( $meta->id );

		return (bool) $wpdb->delete( $db_info['table'], array( $db_info['meta_id_field'] => $meta_id ) );
	}

	/**
	 * Add new piece of meta.
	 *
	 * @param  WC_Data  $object WC_Data object.
	 * @param  stdClass $meta (containing ->key and ->value).
	 * @return int meta ID
	 */
	public function add_meta( &$object, $meta ) {
		global $wpdb;

		$db_info = $this->get_db_info();

		$object_id  = $object->get_id();
		$meta_key   = wp_unslash( wp_slash( $meta->key ) );
		$meta_value = maybe_serialize( is_string( $meta->value ) ? wp_unslash( wp_slash( $meta->value ) ) : $meta->value );

		// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_value,WordPress.DB.SlowDBQuery.slow_db_query_meta_key
		$result = $wpdb->insert(
			$db_info['table'],
			array(
				$db_info['object_id_field'] => $object_id,
				'meta_key'                  => $meta_key,
				'meta_value'                => $meta_value,
			)
		);
		// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_value,WordPress.DB.SlowDBQuery.slow_db_query_meta_key

		return $result ? (int) $wpdb->insert_id : false;
	}

	/**
	 * Update meta.
	 *
	 * @param  WC_Data  $object WC_Data object.
	 * @param  stdClass $meta (containing ->id, ->key and ->value).
	 */
	public function update_meta( &$object, $meta ) {
		global $wpdb;

		if ( ! isset( $meta->id ) || empty( $meta->key ) ) {
			return false;
		}

		// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_value,WordPress.DB.SlowDBQuery.slow_db_query_meta_key
		$data = array(
			'meta_key'   => $meta->key,
			'meta_value' => maybe_serialize( $meta->value ),
		);
		// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_value,WordPress.DB.SlowDBQuery.slow_db_query_meta_key

		$db_info = $this->get_db_info();

		$result = $wpdb->update(
			$db_info['table'],
			$data,
			array( $db_info['meta_id_field'] => $meta->id ),
			'%s',
			'%d'
		);

		return 1 === $result;
	}

	/**
	 * Retrieves metadata by meta ID.
	 *
	 * @param int $meta_id Meta ID.
	 * @return object|bool Metadata object or FALSE if not found.
	 */
	public function get_metadata_by_id( $meta_id ) {
		global $wpdb;

		if ( ! is_numeric( $meta_id ) || floor( $meta_id ) != $meta_id ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
			return false;
		}

		$db_info = $this->get_db_info();

		$meta_id = absint( $meta_id );
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$meta = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT {$db_info['meta_id_field']}, meta_key, meta_value, {$db_info['object_id_field']} FROM {$db_info['table']} WHERE {$db_info['meta_id_field']} = %d",
				$meta_id
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		if ( empty( $meta ) ) {
			return false;
		}

		if ( isset( $meta->meta_value ) ) {
			$meta->meta_value = maybe_unserialize( $meta->meta_value );
		}

		return $meta;
	}
}
Caching/CacheException.php000064400000004320152333011040011462 0ustar00<?php

namespace Automattic\WooCommerce\Caching;

/**
 * Exception thrown by classes derived from ObjectCache.
 */
class CacheException extends \Exception {

	/**
	 * Error messages.
	 *
	 * @var array
	 */
	private $errors;

	/**
	 * The object that threw the exception.
	 *
	 * @var ObjectCache
	 */
	private $thrower;

	/**
	 * The id of the cached object, if available.
	 *
	 * @var int|string|null
	 */
	private $cached_id;

	/**
	 * Creates a new instance of the class.
	 *
	 * @param string          $message The exception message.
	 * @param ObjectCache     $thrower The object that is throwing the exception.
	 * @param int|string|null $cached_id The involved cached object id, if available.
	 * @param array|null      $errors An array of error messages, if available.
	 * @param mixed           $code An error code, if available.
	 * @param \Throwable|null $previous The previous exception, if available.
	 */
	public function __construct( string $message, ObjectCache $thrower, $cached_id = null, ?array $errors = null, $code = 0, \Throwable $previous = null ) {
		$this->errors    = $errors ?? array();
		$this->thrower   = $thrower;
		$this->cached_id = $cached_id;

		parent::__construct( $message, $code, $previous );
	}

	/**
	 * Get a string representation of the exception object.
	 *
	 * @return string String representation of the exception object.
	 */
	public function __toString(): string {
		$cached_id_part = $this->cached_id ? ", id: {$this->cached_id}" : '';
		return "CacheException: [{$this->thrower->get_object_type()}{$cached_id_part}]: {$this->message}";
	}

	/**
	 * Gets the array of error messages passed to the exception constructor.
	 *
	 * @return array Error messages passed to the exception constructor.
	 */
	public function get_errors(): array {
		return $this->errors;
	}

	/**
	 * Gets the object that threw the exception as passed to the exception constructor.
	 *
	 * @return object The object that threw the exception.
	 */
	public function get_thrower(): object {
		return $this->thrower;
	}

	/**
	 * Gets the id of the cached object as passed to the exception constructor.
	 *
	 * @return int|string|null The id of the cached object.
	 */
	public function get_cached_id() {
		return $this->cached_id;
	}
}
Caching/ObjectCache.php000064400000033737152333011040010750 0ustar00<?php

namespace Automattic\WooCommerce\Caching;

/**
 * Base class for caching objects (or associative arrays) that have a unique identifier.
 * At the very least, derived classes need to implement the 'get_object_type' method,
 * but usually it will be convenient to override some of the other protected members.
 *
 * The actual caching is delegated to an instance of CacheEngine. By default WpCacheEngine is used,
 * but a different engine can be used by either overriding the get_cache_engine_instance method
 * or capturing the wc_object_cache_get_engine filter.
 *
 * Objects are identified by ids that are either integers or strings. The actual cache keys passed
 * to the cache engine will be prefixed with the object type and a random string. The 'flush' operation
 * just forces the generation a new prefix and lets the old cached objects expire.
 */
abstract class ObjectCache {

	private const CACHE_PREFIX_OPTION_NAME = 'wp_object_cache_key_prefix_';

	/**
	 * Expiration value to be passed to 'set' to use the value of $default_expiration.
	 */
	public const DEFAULT_EXPIRATION = -1;

	/**
	 * Maximum expiration time value, in seconds, that can be passed to 'set'.
	 */
	public const MAX_EXPIRATION = MONTH_IN_SECONDS;

	/**
	 * This needs to be set in each derived class.
	 *
	 * @var string
	 */
	private $object_type;

	/**
	 * Default value for the duration of the objects in the cache, in seconds
	 * (may not be used depending on the cache engine used WordPress cache implementation).
	 *
	 * @var int
	 */
	protected $default_expiration = HOUR_IN_SECONDS;

	/**
	 * Temporarily used when retrieving data in 'get'.
	 *
	 * @var array
	 */
	private $last_cached_data;

	/**
	 * The cache engine to use.
	 *
	 * @var CacheEngine
	 */
	private $cache_engine = null;

	/**
	 * The prefix to use for cache keys to pass to the cache engine.
	 *
	 * @var string
	 */
	private $cache_key_prefix = null;

	/**
	 * The name of the option used to store the cache prefix.
	 *
	 * @var string
	 */
	private $cache_key_prefix_option_name;

	/**
	 * Gets an identifier for the types of objects cached by this class.
	 * This identifier will be used to compose the keys passed to the cache engine,
	 * to the name of the option that stores the cache prefix, and the names of the hooks used.
	 * It must be unique for each class inheriting from ObjectCache.
	 *
	 * @return string
	 */
	abstract public function get_object_type(): string;

	/**
	 * Creates a new instance of the class.
	 *
	 * @throws CacheException If get_object_type returns null or an empty string.
	 */
	public function __construct() {
		$this->object_type = $this->get_object_type();
		if ( empty( $this->object_type ) ) {
			throw new CacheException( 'Class ' . get_class( $this ) . ' returns an empty value for get_object_type', $this );
		}

		$this->cache_key_prefix_option_name = self::CACHE_PREFIX_OPTION_NAME . $this->object_type;
	}

	/**
	 * Get the default expiration time for cached objects, in seconds.
	 *
	 * @return int
	 */
	public function get_default_expiration_value(): int {
		return $this->default_expiration;
	}

	/**
	 * Get the cache engine to use and cache it internally.
	 *
	 * @return CacheEngine
	 */
	private function get_cache_engine(): CacheEngine {
		if ( null === $this->cache_engine ) {
			$engine = $this->get_cache_engine_instance();

			/**
			 * Filters the underlying cache engine to be used by an instance of ObjectCache.
			 *
			 * @since 6.8.0
			 *
			 * @param CacheEngine $engine The cache engine to be used by default.
			 * @param ObjectCache $cache_instance The instance of ObjectCache that will use the cache engine.
			 * @returns CacheEngine The actual cache engine that will be used.
			 */
			$this->cache_engine = apply_filters( 'wc_object_cache_get_engine', $engine, $this );
		}
		return $this->cache_engine;
	}

	/**
	 * Get the current cache prefix to use, generating one if none is in use yet.
	 *
	 * @return string
	 */
	private function get_cache_key_prefix(): string {
		$value = $this->cache_key_prefix;
		if ( ! $value ) {
			$value = get_option( $this->cache_key_prefix_option_name );
			if ( ! $value ) {
				$value = $this->create_cache_key_prefix();
			}
			$this->cache_key_prefix = $value;
		}
		return $value;
	}

	/**
	 * Generate a prefix for the cache keys to use, containing the object type and a random string,
	 * and store it persistently using an option.
	 *
	 * @return string The generated prefix.
	 * @throws CacheException Can't store the generated prefix.
	 */
	private function create_cache_key_prefix(): string {
		$prefix_variable_part = $this->get_random_string();
		$prefix               = "woocommerce_object_cache|{$this->object_type}|{$prefix_variable_part}|";
		if ( ! update_option( $this->cache_key_prefix_option_name, $prefix ) ) {
			throw new CacheException( "Can't store the key prefix option", $this );
		}
		return $prefix;
	}

	/**
	 * Add an object to the cache, or update an already cached object.
	 *
	 * @param int|string|null $id Id of the object to be cached, if null, get_object_id will be used to get it.
	 * @param object|array    $object The object to be cached.
	 * @param int             $expiration Expiration of the cached data in seconds from the current time, or DEFAULT_EXPIRATION to use the default value.
	 * @return bool True on success, false on error.
	 * @throws CacheException Invalid parameter, or null id was passed and get_object_id returns null too.
	 */
	public function set( $id = null, $object, int $expiration = self::DEFAULT_EXPIRATION ): bool {
		if ( null === $object ) {
			throw new CacheException( "Can't cache a null value", $this, $id );
		}

		if ( ! is_array( $object ) && ! is_object( $object ) ) {
			throw new CacheException( "Can't cache a non-object, non-array value", $this, $id );
		}

		if ( ! is_string( $id ) && ! is_int( $id ) && ! is_null( $id ) ) {
			throw new CacheException( "Object id must be an int, a string, or null for 'set'", $this, $id );
		}

		$this->verify_expiration_value( $expiration );

		if ( null === $id ) {
			$id = $this->get_object_id( $object );
			if ( null === $id ) {
				throw new CacheException( "Null id supplied and the cache class doesn't implement get_object_id", $this );
			}
		}

		$errors = $this->validate( $object );
		if ( null !== $errors && 1 === count( $errors ) ) {
			throw new CacheException( 'Object validation/serialization failed: ' . $errors[0], $this, $id, $errors );
		} elseif ( ! empty( $errors ) ) {
			throw new CacheException( 'Object validation/serialization failed', $this, $id, $errors );
		}

		$data = $this->serialize( $object );

		/**
		 * Filters the serialized object that will be passed by an instance of ObjectCache to its cache engine to be cached.
		 *
		 * @since 6.8.0
		 *
		 * @param array $data The already serialized object data.
		 * @param array|object $object The object before serialization.
		 * @returns array The actual serialized object data that will be passed to the cache engine.
		 */
		$data = apply_filters( "woocommerce_after_serializing_{$this->object_type}_for_caching", $data, $object, $id );

		$this->last_cached_data = $data;
		return $this->get_cache_engine()->cache_object( $this->get_cache_key_prefix() . $id, $data, self::DEFAULT_EXPIRATION === $expiration ? $this->default_expiration : $expiration );
	}

	/**
	 * Check if the given expiration time value is valid, throw an exception if not.
	 *
	 * @param int $expiration Expiration time to check.
	 * @return void
	 * @throws CacheException Expiration time is negative or higher than MAX_EXPIRATION.
	 */
	private function verify_expiration_value( int $expiration ): void {
		if ( self::DEFAULT_EXPIRATION !== $expiration && ( ( $expiration < 1 ) || ( $expiration > self::MAX_EXPIRATION ) ) ) {
			throw new CacheException( 'Invalid expiration value, must be ObjectCache::DEFAULT_EXPIRATION or a value between 1 and ObjectCache::MAX_EXPIRATION', $this );
		}
	}

	/**
	 * Retrieve a cached object, and if no object is cached with the given id,
	 * try to get one via get_from_datastore method or by supplying a callback and then cache it.
	 *
	 * If you want to provide a callable but still use the default expiration value,
	 * pass "ObjectCache::DEFAULT_EXPIRATION" as the second parameter.
	 *
	 * @param int|string    $id The id of the object to retrieve.
	 * @param int           $expiration Expiration of the cached data in seconds from the current time, used if an object is retrieved from datastore and cached.
	 * @param callable|null $get_from_datastore_callback Optional callback to get the object if it's not cached, it must return an object/array or null.
	 * @return object|array|null Cached object, or null if it's not cached and can't be retrieved from datastore or via callback.
	 * @throws CacheException Invalid id parameter.
	 */
	public function get( $id, int $expiration = self::DEFAULT_EXPIRATION, callable $get_from_datastore_callback = null ) {
		if ( ! is_string( $id ) && ! is_int( $id ) ) {
			throw new CacheException( "Object id must be an int or a string for 'get'", $this );
		}

		$this->verify_expiration_value( $expiration );

		$data = $this->get_cache_engine()->get_cached_object( $this->get_cache_key_prefix() . $id );
		if ( null === $data ) {
			if ( $get_from_datastore_callback ) {
				$object = $get_from_datastore_callback( $id );
			} else {
				$object = $this->get_from_datastore( $id );
			}

			if ( null === $object ) {
				return null;
			}

			$this->set( $id, $object, $expiration );
			$data = $this->last_cached_data;
		}

		$object = $this->deserialize( $data );

		/**
		 * Filters the deserialized object that is retrieved from the cache engine of an instance of ObjectCache and will be returned by 'get'.
		 *
		 * @since 6.8.0
		 *
		 * @param array|object $object The object after being deserialized.
		 * @param array $data The serialized object data that was retrieved from the cache engine.
		 * @returns array|object The actual deserialized object data that will be returned by 'get'.
		 */
		return apply_filters( "woocommerce_after_deserializing_{$this->object_type}_from_cache", $object, $data, $id );
	}

	/**
	 * Remove an object from the cache.
	 *
	 * @param int|string $id The id of the object to remove.
	 * @return bool True if the object is removed from the cache successfully, false otherwise (because the object wasn't cached or for other reason).
	 */
	public function remove( $id ): bool {
		$result = $this->get_cache_engine()->delete_cached_object( $this->get_cache_key_prefix() . $id );

		/**
		 * Action triggered by an instance of ObjectCache after an object is (attempted to be) removed from the cache.
		 *
		 * @since 6.8.0
		 *
		 * @param int|string $id The id of the object being removed.
		 * @param bool $result True if the object removal succeeded, false otherwise.
		 */
		do_action( "woocommerce_after_removing_{$this->object_type}_from_cache", $id, $result );

		return $result;
	}

	/**
	 * Remove all the objects from the cache.
	 * This is done by forcing the generation of a new cache key prefix
	 * and leaving the old cached objects to expire.
	 *
	 * @return void
	 */
	public function flush(): void {
		delete_option( $this->cache_key_prefix_option_name );
		$this->cache_key_prefix = null;

		/**
		 * Action triggered by an instance of ObjectCache after it flushes all the cached objects.
		 *
		 * @since 6.8.0
		 *
		 * @param ObjectCache $cache_instance The instance of ObjectCache whose 'flush` method has been called.
		 * @param CacheEngine $engine The cache engine in use.
		 */
		do_action( "woocommerce_after_flushing_{$this->object_type}_cache", $this, $this->get_cache_engine() );
	}

	/**
	 * Is a given object cached?
	 *
	 * @param int|string $id The id of the object to check.
	 * @return bool True if there's a cached object with the specified id.
	 */
	public function is_cached( $id ): bool {
		return $this->get_cache_engine()->is_cached( $this->get_cache_key_prefix() . $id );
	}

	/**
	 * Get the id of an object. This is used by 'set' when a null id is passed.
	 * If the object id can't be determined the method must return null.
	 *
	 * @param array|object $object The object to get the id for.
	 * @return int|string|null
	 */
	protected function get_object_id( $object ) {
		return null;
	}

	/**
	 * Validate an object before it's cached.
	 *
	 * @param array|object $object Object to validate.
	 * @return array|null An array with validation error messages, null or an empty array if there are no errors.
	 */
	protected function validate( $object ): ?array {
		return null;
	}

	/**
	 * Convert an object to a serialized form suitable for caching.
	 * If a class overrides this method it should override 'deserialize' as well.
	 *
	 * @param array|object $object The object to serialize.
	 * @return array The serialized object.
	 */
	protected function serialize( $object ): array {
		return array( 'data' => $object );
	}

	/**
	 * Deserializes a set of object data after having been retrieved from the cache.
	 * If a class overrides this method it should override 'serialize' as well.
	 *
	 * @param array $serialized Serialized object data as it was returned by 'validate_and_serialize'.
	 * @return object|array Deserialized object, ready to be returned by 'get'.
	 */
	protected function deserialize( array $serialized ) {
		return $serialized['data'];
	}

	/**
	 * Get an object from an authoritative data store.
	 * This is used by 'get' if the object isn't cached and no custom object retrieval callback is suupplied.
	 *
	 * @param int|string $id The id of the object to get.
	 * @return array|object|null The retrieved object, or null if it's not possible to retrieve an object by the given id.
	 */
	protected function get_from_datastore( $id ) {
		return null;
	}

	/**
	 * Get the instance of the cache engine to use.
	 *
	 * @return CacheEngine
	 */
	protected function get_cache_engine_instance(): CacheEngine {
		return wc_get_container()->get( WpCacheEngine::class );
	}

	/**
	 * Get a random string to be used to compose the cache key prefix.
	 * It should return a different string each time.
	 *
	 * @return string
	 */
	protected function get_random_string(): string {
		return dechex( microtime( true ) * 1000 ) . bin2hex( random_bytes( 8 ) );
	}
}
Caching/WpCacheEngine.php000064400000001614152333011040011243 0ustar00<?php

namespace Automattic\WooCommerce\Caching;

/**
 * Implementation of CacheEngine that uses the built-in WordPress cache.
 */
class WpCacheEngine implements CacheEngine {

	public const CACHE_GROUP_NAME = 'wc-object-cache';

	// phpcs:disable Squiz.Commenting.FunctionComment.Missing

	public function get_cached_object( string $key ) {
		$value = wp_cache_get( $key, self::CACHE_GROUP_NAME );
		return false === $value ? null : $value;
	}

	public function cache_object( string $key, $object, int $expiration ): bool {
		return wp_cache_set( $key, $object, self::CACHE_GROUP_NAME, $expiration );
	}

	public function delete_cached_object( string $key ): bool {
		return wp_cache_delete( $key, self::CACHE_GROUP_NAME );
	}

	public function is_cached( string $key ): bool {
		return false !== wp_cache_get( $key, self::CACHE_GROUP_NAME );
	}

	// phpcs:enable Squiz.Commenting.FunctionComment.Missing
}
Caching/CacheEngine.php000064400000003017152333011040010733 0ustar00<?php

namespace Automattic\WooCommerce\Caching;

/**
 * Interface for cache engines used by objects inheriting from ObjectCache.
 * Here "object" means either an array or an actual PHP object.
 */
interface CacheEngine {

	/**
	 * Retrieves an object cached under a given key.
	 *
	 * @param string $key They key under which the object to retrieve is cached.
	 * @return array|object|null The cached object, or null if there's no object cached under the passed key.
	 */
	public function get_cached_object( string $key);

	/**
	 * Caches an object under a given key, and with a given expiration.
	 *
	 * @param string       $key The key under which the object will be cached.
	 * @param array|object $object The object to cache.
	 * @param int          $expiration Expiration for the cached object, in seconds.
	 * @return bool True if the object is cached successfully, false otherwise.
	 */
	public function cache_object( string $key, $object, int $expiration): bool;

	/**
	 * Removes a cached object from the cache.
	 *
	 * @param string $key They key under which the object is cached.
	 * @return bool True if the object is removed from the cache successfully, false otherwise (because the object wasn't cached or for other reason).
	 */
	public function delete_cached_object( string $key): bool;

	/**
	 * Checks if an object is cached under a given key.
	 *
	 * @param string $key The key to verify.
	 * @return bool True if there's an object cached under the given key, false otherwise.
	 */
	public function is_cached( string $key): bool;
}
Proxies/ActionsProxy.php000064400000002120152333011040011333 0ustar00<?php
/**
 * ActionsProxy class file.
 */

namespace Automattic\WooCommerce\Proxies;

/**
 * Proxy for interacting with WordPress actions and filters.
 *
 * This class should be used instead of directly accessing the WordPress functions, to ease unit testing.
 */
class ActionsProxy {

	/**
	 * Retrieve the number of times an action is fired.
	 *
	 * @param string $tag The name of the action hook.
	 *
	 * @return int The number of times action hook $tag is fired.
	 */
	public function did_action( $tag ) {
		return did_action( $tag );
	}

	/**
	 * Calls the callback functions that have been added to a filter hook.
	 *
	 * @param string $tag     The name of the filter hook.
	 * @param mixed  $value   The value to filter.
	 * @param mixed  ...$parameters Additional parameters to pass to the callback functions.
	 *
	 * @return mixed The filtered value after all hooked functions are applied to it.
	 */
	public function apply_filters( $tag, $value, ...$parameters ) {
		return apply_filters( $tag, $value, ...$parameters );
	}

	// TODO: Add the rest of the actions and filters related methods.
}
Proxies/LegacyProxy.php000064400000007651152333011040011155 0ustar00<?php
/**
 * LegacyProxy class file.
 */

namespace Automattic\WooCommerce\Proxies;

use Automattic\WooCommerce\Internal\DependencyManagement\Definition;
use \Psr\Container\ContainerInterface;

/**
 * Proxy class to access legacy WooCommerce functionality.
 *
 * This class should be used to interact with code outside the `src` directory, especially functions and classes
 * in the `includes` directory, unless a more specific proxy exists for the functionality at hand (e.g. `ActionsProxy`).
 * Idempotent functions can be executed directly.
 */
class LegacyProxy {

	/**
	 * Gets an instance of a given legacy class.
	 * This must not be used to get instances of classes in the `src` directory.
	 *
	 * If a given class needs a special procedure to get an instance of it,
	 * please add a private get_instance_of_(lowercased_class_name) and it will be
	 * automatically invoked. See also how objects of classes having a static `instance`
	 * method are retrieved, similar approaches can be used as needed to make use
	 * of existing factory methods such as e.g. 'load'.
	 *
	 * @param string $class_name The name of the class to get an instance for.
	 * @param mixed  ...$args Parameters to be passed to the class constructor or to the appropriate internal 'get_instance_of_' method.
	 *
	 * @return object The instance of the class.
	 * @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class.
	 */
	public function get_instance_of( string $class_name, ...$args ) {
		if ( false !== strpos( $class_name, '\\' ) ) {
			throw new \Exception(
				'The LegacyProxy class is not intended for getting instances of classes in the src directory, please use ' .
				Definition::INJECTION_METHOD . ' method injection or the instance of ' . ContainerInterface::class . ' for that.'
			);
		}

		// If a class has a dedicated method to obtain a instance, use it.
		$method = 'get_instance_of_' . strtolower( $class_name );
		if ( method_exists( __CLASS__, $method ) ) {
			return $this->$method( ...$args );
		}

		// If the class is a singleton, use the "instance" method.
		if ( method_exists( $class_name, 'instance' ) ) {
			return $class_name::instance( ...$args );
		}

		// If the class has a "load" method, use it.
		if ( method_exists( $class_name, 'load' ) ) {
			return $class_name::load( ...$args );
		}

		// Fallback to simply creating a new instance of the class.
		return new $class_name( ...$args );
	}

	/**
	 * Get an instance of a class implementing WC_Queue_Interface.
	 *
	 * @return \WC_Queue_Interface The instance.
	 */
	private function get_instance_of_wc_queue_interface() {
		return \WC_Queue::instance();
	}

	/**
	 * Call a user function. This should be used to execute any non-idempotent function, especially
	 * those in the `includes` directory or provided by WordPress.
	 *
	 * @param string $function_name The function to execute.
	 * @param mixed  ...$parameters The parameters to pass to the function.
	 *
	 * @return mixed The result from the function.
	 */
	public function call_function( $function_name, ...$parameters ) {
		return call_user_func_array( $function_name, $parameters );
	}

	/**
	 * Call a static method in a class. This should be used to execute any non-idempotent method in classes
	 * from the `includes` directory.
	 *
	 * @param string $class_name The name of the class containing the method.
	 * @param string $method_name The name of the method.
	 * @param mixed  ...$parameters The parameters to pass to the method.
	 *
	 * @return mixed The result from the method.
	 */
	public function call_static( $class_name, $method_name, ...$parameters ) {
		return call_user_func_array( "$class_name::$method_name", $parameters );
	}

	/**
	 * Get the value of a global.
	 *
	 * @param string $global_name The name of the global to get the value for.
	 * @return mixed The value of the global.
	 */
	public function get_global( string $global_name ) {
		return $GLOBALS[ $global_name ];
	}
}
Utilities/NumberUtil.php000064400000002230152333011040011303 0ustar00<?php
/**
 * A class of utilities for dealing with numbers.
 */

namespace Automattic\WooCommerce\Utilities;

/**
 * A class of utilities for dealing with numbers.
 */
final class NumberUtil {

	/**
	 * Round a number using the built-in `round` function, but unless the value to round is numeric
	 * (a number or a string that can be parsed as a number), apply 'floatval' first to it
	 * (so it will convert it to 0 in most cases).
	 *
	 * This is needed because in PHP 7 applying `round` to a non-numeric value returns 0,
	 * but in PHP 8 it throws an error. Specifically, in WooCommerce we have a few places where
	 * round('') is often executed.
	 *
	 * @param mixed $val The value to round.
	 * @param int   $precision The optional number of decimal digits to round to.
	 * @param int   $mode A constant to specify the mode in which rounding occurs.
	 *
	 * @return float The value rounded to the given precision as a float, or the supplied default value.
	 */
	public static function round( $val, int $precision = 0, int $mode = PHP_ROUND_HALF_UP ) : float {
		if ( ! is_numeric( $val ) ) {
			$val = floatval( $val );
		}
		return round( $val, $precision, $mode );
	}
}
Utilities/FeaturesUtil.php000064400000010766152333011040011646 0ustar00<?php
/**
 * FeaturesUtil class file.
 */

namespace Automattic\WooCommerce\Utilities;

use Automattic\WooCommerce\Internal\Features\FeaturesController;

/**
 * Class with methods that allow to retrieve information about the existing WooCommerce features,
 * also has methods for WooCommerce plugins to declare (in)compatibility with the features.
 */
class FeaturesUtil {

	/**
	 * Get all the existing WooCommerce features.
	 *
	 * Returns an associative array where keys are unique feature ids
	 * and values are arrays with these keys:
	 *
	 * - name
	 * - description
	 * - is_experimental
	 * - is_enabled (if $include_enabled_info is passed as true)
	 *
	 * @param bool $include_experimental Include also experimental/work in progress features in the list.
	 * @param bool $include_enabled_info True to include the 'is_enabled' field in the returned features info.
	 * @returns array An array of information about existing features.
	 */
	public static function get_features( bool $include_experimental = false, bool $include_enabled_info = false ): array {
		return wc_get_container()->get( FeaturesController::class )->get_features( $include_experimental, $include_enabled_info );
	}

	/**
	 * Check if a given feature is currently enabled.
	 *
	 * @param  string $feature_id Unique feature id.
	 * @return bool True if the feature is enabled, false if not or if the feature doesn't exist.
	 */
	public static function feature_is_enabled( string $feature_id ): bool {
		return wc_get_container()->get( FeaturesController::class )->feature_is_enabled( $feature_id );
	}

	/**
	 * Declare (in)compatibility with a given feature for a given plugin.
	 *
	 * This method MUST be executed from inside a handler for the 'before_woocommerce_init' hook and
	 * SHOULD be executed from the main plugin file passing __FILE__ or 'my-plugin/my-plugin.php' for the
	 * $plugin_file argument.
	 *
	 * @param string $feature_id Unique feature id.
	 * @param string $plugin_file The full plugin file path.
	 * @param bool   $positive_compatibility True if the plugin declares being compatible with the feature, false if it declares being incompatible.
	 * @return bool True on success, false on error (feature doesn't exist or not inside the required hook).
	 */
	public static function declare_compatibility( string $feature_id, string $plugin_file, bool $positive_compatibility = true ): bool {
		$plugin_id = wc_get_container()->get( PluginUtil::class )->get_wp_plugin_id( $plugin_file );

		if ( ! $plugin_id ) {
			$logger = wc_get_logger();
			$logger->error( "FeaturesUtil::declare_compatibility: ${plugin_file} is not a known WordPress plugin." );
			return false;
		}

		return wc_get_container()->get( FeaturesController::class )->declare_compatibility( $feature_id, $plugin_id, $positive_compatibility );
	}

	/**
	 * Get the ids of the features that a certain plugin has declared compatibility for.
	 *
	 * This method can't be called before the 'woocommerce_init' hook is fired.
	 *
	 * @param string $plugin_name Plugin name, in the form 'directory/file.php'.
	 * @return array An array having a 'compatible' and an 'incompatible' key, each holding an array of plugin ids.
	 */
	public static function get_compatible_features_for_plugin( string $plugin_name ): array {
		return wc_get_container()->get( FeaturesController::class )->get_compatible_features_for_plugin( $plugin_name );
	}

	/**
	 * Get the names of the plugins that have been declared compatible or incompatible with a given feature.
	 *
	 * @param string $feature_id Feature id.
	 * @return array An array having a 'compatible' and an 'incompatible' key, each holding an array of plugin names.
	 */
	public static function get_compatible_plugins_for_feature( string $feature_id ): array {
		return wc_get_container()->get( FeaturesController::class )->get_compatible_plugins_for_feature( $feature_id );
	}

	/**
	 * Sets a flag indicating that it's allowed to enable features for which incompatible plugins are active
	 * from the WooCommerce feature settings page.
	 */
	public static function allow_enabling_features_with_incompatible_plugins(): void {
		wc_get_container()->get( FeaturesController::class )->allow_enabling_features_with_incompatible_plugins();
	}

	/**
	 * Sets a flag indicating that it's allowed to activate plugins for which incompatible features are enabled
	 * from the WordPress plugins page.
	 */
	public static function allow_activating_plugins_with_incompatible_features(): void {
		wc_get_container()->get( FeaturesController::class )->allow_activating_plugins_with_incompatible_features();
	}
}
Utilities/StringUtil.php000064400000005516152333011040011333 0ustar00<?php
/**
 * A class of utilities for dealing with strings.
 */

namespace Automattic\WooCommerce\Utilities;

/**
 * A class of utilities for dealing with strings.
 */
final class StringUtil {

	/**
	 * Checks to see whether or not a string starts with another.
	 *
	 * @param string $string The string we want to check.
	 * @param string $starts_with The string we're looking for at the start of $string.
	 * @param bool   $case_sensitive Indicates whether the comparison should be case-sensitive.
	 *
	 * @return bool True if the $string starts with $starts_with, false otherwise.
	 */
	public static function starts_with( string $string, string $starts_with, bool $case_sensitive = true ): bool {
		$len = strlen( $starts_with );
		if ( $len > strlen( $string ) ) {
			return false;
		}

		$string = substr( $string, 0, $len );

		if ( $case_sensitive ) {
			return strcmp( $string, $starts_with ) === 0;
		}

		return strcasecmp( $string, $starts_with ) === 0;
	}

	/**
	 * Checks to see whether or not a string ends with another.
	 *
	 * @param string $string The string we want to check.
	 * @param string $ends_with The string we're looking for at the end of $string.
	 * @param bool   $case_sensitive Indicates whether the comparison should be case-sensitive.
	 *
	 * @return bool True if the $string ends with $ends_with, false otherwise.
	 */
	public static function ends_with( string $string, string $ends_with, bool $case_sensitive = true ): bool {
		$len = strlen( $ends_with );
		if ( $len > strlen( $string ) ) {
			return false;
		}

		$string = substr( $string, -$len );

		if ( $case_sensitive ) {
			return strcmp( $string, $ends_with ) === 0;
		}

		return strcasecmp( $string, $ends_with ) === 0;
	}

	/**
	 * Checks if one string is contained into another at any position.
	 *
	 * @param string $string The string we want to check.
	 * @param string $contained The string we're looking for inside $string.
	 * @param bool   $case_sensitive Indicates whether the comparison should be case-sensitive.
	 * @return bool True if $contained is contained inside $string, false otherwise.
	 */
	public static function contains( string $string, string $contained, bool $case_sensitive = true ): bool {
		if ( $case_sensitive ) {
			return false !== strpos( $string, $contained );
		} else {
			return false !== stripos( $string, $contained );
		}
	}

	/**
	 * Get the name of a plugin in the form 'directory/file.php', as in the keys of the array returned by 'get_plugins'.
	 *
	 * @param string $plugin_file_path The path of the main plugin file (can be passed as __FILE__ from the plugin itself).
	 * @return string The name of the plugin in the form 'directory/file.php'.
	 */
	public static function plugin_name_from_plugin_file( string $plugin_file_path ): string {
		return basename( dirname( $plugin_file_path ) ) . DIRECTORY_SEPARATOR . basename( $plugin_file_path );
	}
}
Utilities/OrderUtil.php000064400000010343152333011040011132 0ustar00<?php
/**
 * A class of utilities for dealing with orders.
 */

namespace Automattic\WooCommerce\Utilities;

use Automattic\WooCommerce\Internal\Admin\Orders\PageController;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\Utilities\COTMigrationUtil;
use WC_Order;
use WP_Post;

/**
 * A class of utilities for dealing with orders.
 */
final class OrderUtil {

	/**
	 * Helper function to get screen name of orders page in wp-admin.
	 *
	 * @return string
	 */
	public static function get_order_admin_screen() : string {
		return wc_get_container()->get( COTMigrationUtil::class )->get_order_admin_screen();
	}


	/**
	 * Helper function to get whether custom order tables are enabled or not.
	 *
	 * @return bool
	 */
	public static function custom_orders_table_usage_is_enabled() : bool {
		return wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled();
	}

	/**
	 * Checks if posts and order custom table sync is enabled and there are no pending orders.
	 *
	 * @return bool
	 */
	public static function is_custom_order_tables_in_sync() : bool {
		return wc_get_container()->get( COTMigrationUtil::class )->is_custom_order_tables_in_sync();
	}

	/**
	 * Gets value of a meta key from WC_Data object if passed, otherwise from the post object.
	 * This helper function support backward compatibility for meta box functions, when moving from posts based store to custom tables.
	 *
	 * @param WP_Post|null  $post Post object, meta will be fetched from this only when `$data` is not passed.
	 * @param \WC_Data|null $data WC_Data object, will be preferred over post object when passed.
	 * @param string        $key Key to fetch metadata for.
	 * @param bool          $single Whether metadata is single.
	 *
	 * @return array|mixed|string Value of the meta key.
	 */
	public static function get_post_or_object_meta( ?WP_Post $post, ?\WC_Data $data, string $key, bool $single ) {
		return wc_get_container()->get( COTMigrationUtil::class )->get_post_or_object_meta( $post, $data, $key, $single );
	}

	/**
	 * Helper function to initialize the global $theorder object, mostly used during order meta boxes rendering.
	 *
	 * @param WC_Order|WP_Post $post_or_order_object Post or order object.
	 *
	 * @return bool|WC_Order|WC_Order_Refund WC_Order object.
	 */
	public static function init_theorder_object( $post_or_order_object ) {
		return wc_get_container()->get( COTMigrationUtil::class )->init_theorder_object( $post_or_order_object );
	}

	/**
	 * Helper function to id from an post or order object.
	 *
	 * @param WP_Post/WC_Order $post_or_order_object WP_Post/WC_Order object to get ID for.
	 *
	 * @return int Order or post ID.
	 */
	public static function get_post_or_order_id( $post_or_order_object ) : int {
		return wc_get_container()->get( COTMigrationUtil::class )->get_post_or_order_id( $post_or_order_object );
	}

	/**
	 * Checks if passed id, post or order object is a WC_Order object.
	 *
	 * @param int|WP_Post|WC_Order $order_id Order ID, post object or order object.
	 * @param string[]             $types    Types to match against.
	 *
	 * @return bool Whether the passed param is an order.
	 */
	public static function is_order( $order_id, $types = array( 'shop_order' ) ) {
		return wc_get_container()->get( COTMigrationUtil::class )->is_order( $order_id, $types );
	}

	/**
	 * Returns type pf passed id, post or order object.
	 *
	 * @param int|WP_Post|WC_Order $order_id Order ID, post object or order object.
	 *
	 * @return string|null Type of the order.
	 */
	public static function get_order_type( $order_id ) {
		return wc_get_container()->get( COTMigrationUtil::class )->get_order_type( $order_id );
	}

	/**
	 * Helper method to generate admin url for an order.
	 *
	 * @param int $order_id Order ID.
	 *
	 * @return string Admin url for an order.
	 */
	public static function get_order_admin_edit_url( int $order_id ) : string {
		return wc_get_container()->get( PageController::class )->get_edit_url( $order_id );
	}

	/**
	 * Helper method to generate admin URL for new order.
	 *
	 * @return string Link for new order.
	 */
	public static function get_order_admin_new_url() : string {
		return wc_get_container()->get( PageController::class )->get_new_page_url();
	}
}
Utilities/ArrayUtil.php000064400000025714152333011040011145 0ustar00<?php
/**
 * A class of utilities for dealing with arrays.
 */

namespace Automattic\WooCommerce\Utilities;

/**
 * A class of utilities for dealing with arrays.
 */
class ArrayUtil {

	/**
	 * Automatic selector type for the 'select' method.
	 */
	public const SELECT_BY_AUTO = 0;

	/**
	 * Object method selector type for the 'select' method.
	 */
	public const SELECT_BY_OBJECT_METHOD = 1;

	/**
	 * Object property selector type for the 'select' method.
	 */
	public const SELECT_BY_OBJECT_PROPERTY = 2;

	/**
	 * Array key selector type for the 'select' method.
	 */
	public const SELECT_BY_ARRAY_KEY = 3;

	/**
	 * Get a value from an nested array by specifying the entire key hierarchy with '::' as separator.
	 *
	 * E.g. for [ 'foo' => [ 'bar' => [ 'fizz' => 'buzz' ] ] ] the value for key 'foo::bar::fizz' would be 'buzz'.
	 *
	 * @param array  $array The array to get the value from.
	 * @param string $key The complete key hierarchy, using '::' as separator.
	 * @param mixed  $default The value to return if the key doesn't exist in the array.
	 *
	 * @return mixed The retrieved value, or the supplied default value.
	 * @throws \Exception $array is not an array.
	 */
	public static function get_nested_value( array $array, string $key, $default = null ) {
		$key_stack = explode( '::', $key );
		$subkey    = array_shift( $key_stack );

		if ( isset( $array[ $subkey ] ) ) {
			$value = $array[ $subkey ];

			if ( count( $key_stack ) ) {
				foreach ( $key_stack as $subkey ) {
					if ( is_array( $value ) && isset( $value[ $subkey ] ) ) {
						$value = $value[ $subkey ];
					} else {
						$value = $default;
						break;
					}
				}
			}
		} else {
			$value = $default;
		}

		return $value;
	}

	/**
	 * Checks if a given key exists in an array and its value can be evaluated as 'true'.
	 *
	 * @param array  $array The array to check.
	 * @param string $key The key for the value to check.
	 * @return bool True if the key exists in the array and the value can be evaluated as 'true'.
	 */
	public static function is_truthy( array $array, string $key ) {
		return isset( $array[ $key ] ) && $array[ $key ];
	}

	/**
	 * Gets the value for a given key from an array, or a default value if the key doesn't exist in the array.
	 *
	 * @param array  $array The array to get the value from.
	 * @param string $key The key to use to retrieve the value.
	 * @param null   $default The default value to return if the key doesn't exist in the array.
	 * @return mixed|null The value for the key, or the default value passed.
	 */
	public static function get_value_or_default( array $array, string $key, $default = null ) {
		return isset( $array[ $key ] ) ? $array[ $key ] : $default;
	}

	/**
	 * Converts an array of numbers to a human-readable range, such as "1,2,3,5" to "1-3, 5". It also supports
	 * floating point numbers, however with some perhaps unexpected / undefined behaviour if used within a range.
	 * Source: https://stackoverflow.com/a/34254663/4574
	 *
	 * @param array     $items    An array (in any order, see $sort) of individual numbers.
	 * @param string    $item_separator  The string that separates sequential range groups.  Defaults to ', '.
	 * @param string    $range_separator The string that separates ranges.  Defaults to '-'.  A plausible example otherwise would be ' to '.
	 * @param bool|true $sort     Sort the array prior to iterating?  You'll likely always want to sort, but if not, you can set this to false.
	 *
	 * @return string
	 */
	public static function to_ranges_string( array $items, string $item_separator = ', ', string $range_separator = '-', bool $sort = true ): string {
		if ( $sort ) {
			sort( $items );
		}

		$point = null;
		$range = false;
		$str   = '';

		foreach ( $items as $i ) {
			if ( null === $point ) {
				$str .= $i;
			} elseif ( ( $point + 1 ) === $i ) {
				$range = true;
			} else {
				if ( $range ) {
					$str  .= $range_separator . $point;
					$range = false;
				}
				$str .= $item_separator . $i;
			}
			$point = $i;
		}

		if ( $range ) {
			$str .= $range_separator . $point;
		}

		return $str;
	}

	/**
	 * Helper function to generate a callback which can be executed on an array to select a value from each item.
	 *
	 * @param string $selector_name Field/property/method name to select.
	 * @param int    $selector_type Selector type.
	 *
	 * @return \Closure Callback to select the value.
	 */
	private static function get_selector_callback( string $selector_name, int $selector_type = self::SELECT_BY_AUTO ): \Closure {
		if ( self::SELECT_BY_OBJECT_METHOD === $selector_type ) {
			$callback = function( $item ) use ( $selector_name ) {
				return $item->$selector_name();
			};
		} elseif ( self::SELECT_BY_OBJECT_PROPERTY === $selector_type ) {
			$callback = function( $item ) use ( $selector_name ) {
				return $item->$selector_name;
			};
		} elseif ( self::SELECT_BY_ARRAY_KEY === $selector_type ) {
			$callback = function( $item ) use ( $selector_name ) {
				return $item[ $selector_name ];
			};
		} else {
			$callback = function( $item ) use ( $selector_name ) {
				if ( is_array( $item ) ) {
					return $item[ $selector_name ];
				} elseif ( method_exists( $item, $selector_name ) ) {
					return $item->$selector_name();
				} else {
					return $item->$selector_name;
				}
			};
		}
		return $callback;
	}

	/**
	 * Select one single value from all the items in an array of either arrays or objects based on a selector.
	 * For arrays, the selector is a key name; for objects, the selector can be either a method name or a property name.
	 *
	 * @param array  $items Items to apply the selection to.
	 * @param string $selector_name Key, method or property name to use as a selector.
	 * @param int    $selector_type Selector type, one of the SELECT_BY_* constants.
	 * @return array The selected values.
	 */
	public static function select( array $items, string $selector_name, int $selector_type = self::SELECT_BY_AUTO ): array {
		$callback = self::get_selector_callback( $selector_name, $selector_type );
		return array_map( $callback, $items );
	}

	/**
	 * Returns a new assoc array with format [ $key1 => $item1, $key2 => $item2, ... ] where $key is the value of the selector and items are original items passed.
	 *
	 * @param array  $items Items to use for conversion.
	 * @param string $selector_name Key, method or property name to use as a selector.
	 * @param int    $selector_type Selector type, one of the SELECT_BY_* constants.
	 *
	 * @return array The converted assoc array.
	 */
	public static function select_as_assoc( array $items, string $selector_name, int $selector_type = self::SELECT_BY_AUTO ): array {
		$selector_callback = self::get_selector_callback( $selector_name, $selector_type );
		$result            = array();
		foreach ( $items as $item ) {
			$key = $selector_callback( $item );
			self::ensure_key_is_array( $result, $key );
			$result[ $key ][] = $item;
		}
		return $result;
	}

	/**
	 * Returns whether two assoc array are same. The comparison is done recursively by keys, and the functions returns on first difference found.
	 *
	 * @param array $array1 First array to compare.
	 * @param array $array2 Second array to compare.
	 * @param bool  $strict Whether to use strict comparison.
	 *
	 * @return bool Whether the arrays are different.
	 */
	public static function deep_compare_array_diff( array $array1, array $array2, bool $strict = true ) {
		return self::deep_compute_or_compare_array_diff( $array1, $array2, true, $strict );
	}

	/**
	 * Computes difference between two assoc arrays recursively. Similar to PHP's native assoc_array_diff, but also supports nested arrays.
	 *
	 * @param array $array1 First array.
	 * @param array $array2 Second array.
	 * @param bool  $strict Whether to also match type of values.
	 *
	 * @return array The difference between the two arrays.
	 */
	public static function deep_assoc_array_diff( array $array1, array $array2, bool $strict = true ): array {
		return self::deep_compute_or_compare_array_diff( $array1, $array2, false, $strict );
	}

	/**
	 * Helper method to compare to compute difference between two arrays. Comparison is done recursively.
	 *
	 * @param array $array1 First array.
	 * @param array $array2 Second array.
	 * @param bool  $compare Whether to compare the arrays. If true, then function will return false on first difference, in order to be slightly more efficient.
	 * @param bool  $strict Whether to do string comparison.
	 *
	 * @return array|bool The difference between the two arrays, or if array are same, depending upon $compare param.
	 */
	private static function deep_compute_or_compare_array_diff( array $array1, array $array2, bool $compare, bool $strict = true ) {
		$diff = array();
		foreach ( $array1 as $key => $value ) {
			if ( is_array( $value ) ) {
				if ( ! array_key_exists( $key, $array2 ) || ! is_array( $array2[ $key ] ) ) {
					if ( $compare ) {
						return true;
					}
					$diff[ $key ] = $value;
				}
				$new_diff = self::deep_assoc_array_diff( $value, $array2[ $key ], $strict );
				if ( ! empty( $new_diff ) ) {
					if ( $compare ) {
						return true;
					}
					$diff[ $key ] = $new_diff;
				}
			} elseif ( $strict ) {
				if ( ! array_key_exists( $key, $array2 ) || $value !== $array2[ $key ] ) {
					if ( $compare ) {
						return true;
					}
					$diff[ $key ] = $value;
				}
			} else {
				// phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison -- Intentional when $strict is false.
				if ( ! array_key_exists( $key, $array2 ) || $value != $array2[ $key ] ) {
					if ( $compare ) {
						return true;
					}
					$diff[ $key ] = $value;
				}
			}
		}

		return $compare ? false : $diff;
	}

	/**
	 * Push a value to an array, but only if the value isn't in the array already.
	 *
	 * @param array $array The array.
	 * @param mixed $value The value to maybe push.
	 * @return bool True if the value has been added to the array, false if the value was already in the array.
	 */
	public static function push_once( array &$array, $value ) : bool {
		if ( in_array( $value, $array, true ) ) {
			return false;
		}

		$array[] = $value;
		return true;
	}

	/**
	 * Ensure that an associative array has a given key, and if not, set the key to an empty array.
	 *
	 * @param array  $array The array to check.
	 * @param string $key The key to check.
	 * @param bool   $throw_if_existing_is_not_array If true, an exception will be thrown if the key already exists in the array but the value is not an array.
	 * @return bool True if the key has been added to the array, false if not (the key already existed).
	 * @throws \Exception The key already exists in the array but the value is not an array.
	 */
	public static function ensure_key_is_array( array &$array, string $key, bool $throw_if_existing_is_not_array = false ): bool {
		if ( ! isset( $array[ $key ] ) ) {
			$array[ $key ] = array();
			return true;
		}

		if ( $throw_if_existing_is_not_array && ! is_array( $array[ $key ] ) ) {
			$type = is_object( $array[ $key ] ) ? get_class( $array[ $key ] ) : gettype( $array[ $key ] );
			throw new \Exception( "Array key exists but it's not an array, it's a {$type}" );
		}

		return false;
	}
}

Utilities/PluginUtil.php000064400000012373152333011040011322 0ustar00<?php
/**
 * A class of utilities for dealing with plugins.
 */

namespace Automattic\WooCommerce\Utilities;

use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Proxies\LegacyProxy;

/**
 * A class of utilities for dealing with plugins.
 */
class PluginUtil {

	use AccessiblePrivateMethods;

	/**
	 * The LegacyProxy instance to use.
	 *
	 * @var LegacyProxy
	 */
	private $proxy;

	/**
	 * The cached list of WooCommerce aware plugin ids.
	 *
	 * @var null|array
	 */
	private $woocommerce_aware_plugins = null;

	/**
	 * The cached list of enabled WooCommerce aware plugin ids.
	 *
	 * @var null|array
	 */
	private $woocommerce_aware_active_plugins = null;

	/**
	 * Creates a new instance of the class.
	 */
	public function __construct() {
		self::add_action( 'activated_plugin', array( $this, 'handle_plugin_de_activation' ), 10, 0 );
		self::add_action( 'deactivated_plugin', array( $this, 'handle_plugin_de_activation' ), 10, 0 );
	}

	/**
	 * Initialize the class instance.
	 *
	 * @internal
	 *
	 * @param LegacyProxy $proxy The instance of LegacyProxy to use.
	 */
	final public function init( LegacyProxy $proxy ) {
		$this->proxy = $proxy;
		require_once ABSPATH . WPINC . '/plugin.php';
	}

	/**
	 * Get a list with the names of the WordPress plugins that are WooCommerce aware
	 * (they have a "WC tested up to" header).
	 *
	 * @param bool $active_only True to return only active plugins, false to return all the active plugins.
	 * @return string[] A list of plugin ids (path/file.php).
	 */
	public function get_woocommerce_aware_plugins( bool $active_only = false ): array {
		if ( is_null( $this->woocommerce_aware_plugins ) ) {
			$all_plugins = $this->proxy->call_function( 'get_plugins' );

			$this->woocommerce_aware_plugins =
				array_keys(
					array_filter(
						$all_plugins,
						array( $this, 'is_woocommerce_aware_plugin' )
					)
				);

			$this->woocommerce_aware_active_plugins =
				array_values(
					array_filter(
						$this->woocommerce_aware_plugins,
						function ( $plugin_name ) {
							return $this->proxy->call_function( 'is_plugin_active', $plugin_name );
						}
					)
				);
		}

		return $active_only ? $this->woocommerce_aware_active_plugins : $this->woocommerce_aware_plugins;
	}

	/**
	 * Get the printable name of a plugin.
	 *
	 * @param string $plugin_id Plugin id (path/file.php).
	 * @return string Printable plugin name, or the plugin id itself if printable name is not available.
	 */
	public function get_plugin_name( string $plugin_id ): string {
		$plugin_data = $this->proxy->call_function( 'get_plugin_data', WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_id );
		return ArrayUtil::get_value_or_default( $plugin_data, 'Name', $plugin_id );
	}

	/**
	 * Check if a plugin is WooCommerce aware.
	 *
	 * @param string|array $plugin_file_or_data Plugin id (path/file.php) or plugin data (as returned by get_plugins).
	 * @return bool True if the plugin exists and is WooCommerce aware.
	 * @throws \Exception The input is neither a string nor an array.
	 */
	public function is_woocommerce_aware_plugin( $plugin_file_or_data ): bool {
		if ( is_string( $plugin_file_or_data ) ) {
			return in_array( $plugin_file_or_data, $this->get_woocommerce_aware_plugins(), true );
		} elseif ( is_array( $plugin_file_or_data ) ) {
			return '' !== ArrayUtil::get_value_or_default( $plugin_file_or_data, 'WC tested up to', '' );
		} else {
			throw new \Exception( 'is_woocommerce_aware_plugin requires a plugin name or an array of plugin data as input' );
		}
	}

	/**
	 * Match plugin identifier passed as a parameter with the output from `get_plugins()`.
	 *
	 * @param string $plugin_file Plugin identifier, either 'my-plugin/my-plugin.php', or output from __FILE__.
	 *
	 * @return string|false Key from the array returned by `get_plugins` if matched. False if no match.
	 */
	public function get_wp_plugin_id( $plugin_file ) {
		$wp_plugins = array_keys( $this->proxy->call_function( 'get_plugins' ) );

		// Try to match plugin_basename().
		$plugin_basename = $this->proxy->call_function( 'plugin_basename', $plugin_file );
		if ( array_key_exists( $plugin_basename, $wp_plugins ) ) {
			return $plugin_basename;
		}

		// Try to match by the my-file/my-file.php (dir + file name), then by my-file.php (file name only).
		$plugin_file     = str_replace( array( '\\', '/' ), DIRECTORY_SEPARATOR, $plugin_file );
		$file_name_parts = explode( DIRECTORY_SEPARATOR, $plugin_file );
		$file_name       = array_pop( $file_name_parts );
		$directory_name  = array_pop( $file_name_parts );
		$full_matches    = array();
		$partial_matches = array();
		foreach ( $wp_plugins as $wp_plugin ) {
			if ( false !== strpos( $wp_plugin, $directory_name . DIRECTORY_SEPARATOR . $file_name ) ) {
				$full_matches[] = $wp_plugin;
			}

			if ( false !== strpos( $wp_plugin, $file_name ) ) {
				$partial_matches[] = $wp_plugin;
			}
		}

		if ( 1 === count( $full_matches ) ) {
			return $full_matches[0];
		}

		if ( 1 === count( $partial_matches ) ) {
			return $partial_matches[0];
		}

		return false;
	}

	/**
	 * Handle plugin activation and deactivation by clearing the WooCommerce aware plugin ids cache.
	 */
	private function handle_plugin_de_activation(): void {
		$this->woocommerce_aware_plugins        = null;
		$this->woocommerce_aware_active_plugins = null;
	}
}
Container.php000064400000012463152333011040007175 0ustar00<?php
/**
 * Container class file.
 */

namespace Automattic\WooCommerce;

use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\COTMigrationServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\DownloadPermissionsAdjusterServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\AssignDefaultCategoryServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\FeaturesServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrdersControllersServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrderAdminServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrderMetaBoxServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ObjectCacheServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrdersDataStoreServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OptionSanitizerServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProductAttributesLookupServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProductDownloadsServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProductReviewsServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProxiesServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\RestockRefundedItemsAdjusterServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\UtilsClassesServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\BatchProcessingServiceProvider;

/**
 * PSR11 compliant dependency injection container for WooCommerce.
 *
 * Classes in the `src` directory should specify dependencies from that directory via an 'init' method having arguments
 * with type hints. If an instance of the container itself is needed, the type hint to use is \Psr\Container\ContainerInterface.
 *
 * Classes in the `src` directory should interact with anything outside (especially code in the `includes` directory
 * and WordPress functions) by using the classes in the `Proxies` directory. The exception is idempotent
 * functions (e.g. `wp_parse_url`), those can be used directly.
 *
 * Classes in the `includes` directory should use the `wc_get_container` function to get the instance of the container when
 * they need to get an instance of a class from the `src` directory.
 *
 * Class registration should be done via service providers that inherit from Automattic\WooCommerce\Internal\DependencyManagement
 * and those should go in the `src\Internal\DependencyManagement\ServiceProviders` folder unless there's a good reason
 * to put them elsewhere. All the service provider class names must be in the `SERVICE_PROVIDERS` constant.
 */
final class Container {
	/**
	 * The list of service provider classes to register.
	 *
	 * @var string[]
	 */
	private $service_providers = array(
		AssignDefaultCategoryServiceProvider::class,
		DownloadPermissionsAdjusterServiceProvider::class,
		OptionSanitizerServiceProvider::class,
		OrdersDataStoreServiceProvider::class,
		ProductAttributesLookupServiceProvider::class,
		ProductDownloadsServiceProvider::class,
		ProductReviewsServiceProvider::class,
		ProxiesServiceProvider::class,
		RestockRefundedItemsAdjusterServiceProvider::class,
		UtilsClassesServiceProvider::class,
		COTMigrationServiceProvider::class,
		OrdersControllersServiceProvider::class,
		ObjectCacheServiceProvider::class,
		BatchProcessingServiceProvider::class,
		OrderMetaBoxServiceProvider::class,
		OrderAdminServiceProvider::class,
		FeaturesServiceProvider::class,
	);

	/**
	 * The underlying container.
	 *
	 * @var \League\Container\Container
	 */
	private $container;

	/**
	 * Class constructor.
	 */
	public function __construct() {
		$this->container = new ExtendedContainer();

		// Add ourselves as the shared instance of ContainerInterface,
		// register everything else using service providers.

		$this->container->share( __CLASS__, $this );

		foreach ( $this->service_providers as $service_provider_class ) {
			$this->container->addServiceProvider( $service_provider_class );
		}
	}

	/**
	 * Finds an entry of the container by its identifier and returns it.
	 *
	 * @param string $id Identifier of the entry to look for.
	 *
	 * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
	 * @throws Psr\Container\ContainerExceptionInterface Error while retrieving the entry.
	 *
	 * @return mixed Entry.
	 */
	public function get( string $id ): object {
		return $this->container->get( $id );
	}

	/**
	 * Returns true if the container can return an entry for the given identifier.
	 * Returns false otherwise.
	 *
	 * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
	 * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
	 *
	 * @param string $id Identifier of the entry to look for.
	 *
	 * @return bool
	 */
	public function has( string $id ): bool {
		return $this->container->has( $id );
	}
}
Packages.php000064400000007632152333011040006773 0ustar00<?php
/**
 * Loads WooCommerce packages from the /packages directory. These are packages developed outside of core.
 */

namespace Automattic\WooCommerce;

defined( 'ABSPATH' ) || exit;

/**
 * Packages class.
 *
 * @since 3.7.0
 */
class Packages {

	/**
	 * Static-only class.
	 */
	private function __construct() {}

	/**
	 * Array of package names and their main package classes.
	 *
	 * @var array Key is the package name/directory, value is the main package class which handles init.
	 */
	protected static $packages = array(
		'woocommerce-blocks'   => '\\Automattic\\WooCommerce\\Blocks\\Package'
	);

	/**
	 * Init the package loader.
	 *
	 * @since 3.7.0
	 */
	public static function init() {
		add_action( 'plugins_loaded', array( __CLASS__, 'on_init' ) );
	}

	/**
	 * Callback for WordPress init hook.
	 */
	public static function on_init() {
		self::load_packages();
	}

	/**
	 * Checks a package exists by looking for it's directory.
	 *
	 * @param string $package Package name.
	 * @return boolean
	 */
	public static function package_exists( $package ) {
		return file_exists( dirname( __DIR__ ) . '/packages/' . $package );
	}

	/**
	 * Loads packages after plugins_loaded hook.
	 *
	 * Each package should include an init file which loads the package so it can be used by core.
	 */
	protected static function load_packages() {
		// Initialize WooCommerce Admin.
		\Automattic\WooCommerce\Admin\Composer\Package::init();

		foreach ( self::$packages as $package_name => $package_class ) {
			if ( ! self::package_exists( $package_name ) ) {
				self::missing_package( $package_name );
				continue;
			}
			call_user_func( array( $package_class, 'init' ) );
		}

		// Proxies "activated_plugin" hook for embedded packages listen on WC plugin activation
		// https://github.com/woocommerce/woocommerce/issues/28697.
		if ( is_admin() ) {
			$activated_plugin = get_transient( 'woocommerce_activated_plugin' );
			if ( $activated_plugin ) {
				delete_transient( 'woocommerce_activated_plugin' );

				/**
				 * WooCommerce is activated hook.
				 *
				 * @since 5.0.0
				 * @param bool $activated_plugin Activated plugin path,
				 *                               generally woocommerce/woocommerce.php.
				 */
				do_action( 'woocommerce_activated_plugin', $activated_plugin );
			}
		}
	}

	/**
	 * If a package is missing, add an admin notice.
	 *
	 * @param string $package Package name.
	 */
	protected static function missing_package( $package ) {
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			error_log(  // phpcs:ignore
				sprintf(
					/* Translators: %s package name. */
					esc_html__( 'Missing the WooCommerce %s package', 'woocommerce' ),
					'<code>' . esc_html( $package ) . '</code>'
				) . ' - ' . esc_html__( 'Your installation of WooCommerce is incomplete. If you installed WooCommerce from GitHub, please refer to this document to set up your development environment: https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment', 'woocommerce' )
			);
		}
		add_action(
			'admin_notices',
			function() use ( $package ) {
				?>
				<div class="notice notice-error">
					<p>
						<strong>
							<?php
							printf(
								/* Translators: %s package name. */
								esc_html__( 'Missing the WooCommerce %s package', 'woocommerce' ),
								'<code>' . esc_html( $package ) . '</code>'
							);
							?>
						</strong>
						<br>
						<?php
						printf(
							/* translators: 1: is a link to a support document. 2: closing link */
							esc_html__( 'Your installation of WooCommerce is incomplete. If you installed WooCommerce from GitHub, %1$splease refer to this document%2$s to set up your development environment.', 'woocommerce' ),
							'<a href="' . esc_url( 'https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment' ) . '" target="_blank" rel="noopener noreferrer">',
							'</a>'
						);
						?>
					</p>
				</div>
				<?php
			}
		);
	}
}
Autoloader.php000064400000003701152333011040007345 0ustar00<?php
/**
 * Includes the composer Autoloader used for packages and classes in the src/ directory.
 */

namespace Automattic\WooCommerce;

defined( 'ABSPATH' ) || exit;

/**
 * Autoloader class.
 *
 * @since 3.7.0
 */
class Autoloader {

	/**
	 * Static-only class.
	 */
	private function __construct() {}

	/**
	 * Require the autoloader and return the result.
	 *
	 * If the autoloader is not present, let's log the failure and display a nice admin notice.
	 *
	 * @return boolean
	 */
	public static function init() {
		$autoloader = dirname( __DIR__ ) . '/vendor/autoload_packages.php';

		if ( ! is_readable( $autoloader ) ) {
			self::missing_autoloader();
			return false;
		}

		$autoloader_result = require $autoloader;
		if ( ! $autoloader_result ) {
			return false;
		}

		return $autoloader_result;
	}

	/**
	 * If the autoloader is missing, add an admin notice.
	 */
	protected static function missing_autoloader() {
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			error_log(  // phpcs:ignore
				esc_html__( 'Your installation of WooCommerce is incomplete. If you installed WooCommerce from GitHub, please refer to this document to set up your development environment: https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment', 'woocommerce' )
			);
		}
		add_action(
			'admin_notices',
			function() {
				?>
				<div class="notice notice-error">
					<p>
						<?php
						printf(
							/* translators: 1: is a link to a support document. 2: closing link */
							esc_html__( 'Your installation of WooCommerce is incomplete. If you installed WooCommerce from GitHub, %1$splease refer to this document%2$s to set up your development environment.', 'woocommerce' ),
							'<a href="' . esc_url( 'https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment' ) . '" target="_blank" rel="noopener noreferrer">',
							'</a>'
						);
						?>
					</p>
				</div>
				<?php
			}
		);
	}
}
Database/Migrations/MetaToCustomTableMigrator.php000064400000075026152333011040016140 0ustar00<?php
/**
 * Generic migration class to move any entity, entity_meta table combination to custom table.
 */

namespace Automattic\WooCommerce\Database\Migrations;

/**
 * Base class for implementing migrations from the standard WordPress meta table
 * to custom structured tables.
 *
 * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
 */
abstract class MetaToCustomTableMigrator extends TableMigrator {

	/**
	 * Config for tables being migrated and migrated from. See __construct() for detailed config.
	 *
	 * @var array
	 */
	protected $schema_config;

	/**
	 * Meta config, see __construct for detailed config.
	 *
	 * @var array
	 */
	protected $meta_column_mapping;

	/**
	 * Column mapping from source table to destination custom table. See __construct for detailed config.
	 *
	 * @var array
	 */
	protected $core_column_mapping;

	/**
	 * MetaToCustomTableMigrator constructor.
	 */
	public function __construct() {
		$this->schema_config       = MigrationHelper::escape_schema_for_backtick( $this->get_schema_config() );
		$this->meta_column_mapping = $this->get_meta_column_config();
		$this->core_column_mapping = $this->get_core_column_mapping();
	}

	/**
	 * Specify schema config the source and destination table.
	 *
	 * @return array Schema, must of the form:
	 * array(
		'source' => array(
			'entity' => array(
				'table_name' => $source_table_name,
				'meta_rel_column' => $column_meta, Name of column in source table which is referenced by meta table.
				'destination_rel_column' => $column_dest, Name of column in source table which is refenced by destination table,
				'primary_key' => $primary_key, Primary key of the source table
			),
			'meta' => array(
				'table' => $meta_table_name,
				'meta_key_column' => $meta_key_column_name,
				'meta_value_column' => $meta_value_column_name,
				'entity_id_column' => $entity_id_column, Name of the column having entity IDs.
			),
		),
		'destination' => array(
			'table_name' => $table_name, Name of destination table,
			'source_rel_column' => $column_source_id, Name of the column in destination table which is referenced by source table.
			'primary_key' => $table_primary_key,
			'primary_key_type' => $type bool|int|string|decimal
		)
	 */
	abstract protected function get_schema_config(): array;

	/**
	 * Specify column config from the source table.
	 *
	 * @return array Config, must be of the form:
	 * array(
	 *  '$source_column_name_1' => array( // $source_column_name_1 is column name in source table, or a select statement.
	 *      'type' => 'type of value, could be string/int/date/float.',
	 *      'destination' => 'name of the column in column name where this data should be inserted in.',
	 *  ),
	 *  '$source_column_name_2' => array(
	 *          ......
	 *  ),
	 *  ....
	 * ).
	 */
	abstract protected function get_core_column_mapping(): array;

	/**
	 * Specify meta keys config from source meta table.
	 *
	 * @return array Config, must be of the form.
	 * array(
	 *  '$meta_key_1' => array(  // $meta_key_1 is the name of meta_key in source meta table.
	 *          'type' => 'type of value, could be string/int/date/float',
	 *          'destination' => 'name of the column in column name where this data should be inserted in.',
	 *  ),
	 *  '$meta_key_2' => array(
	 *          ......
	 *  ),
	 *  ....
	 * ).
	 */
	abstract protected function get_meta_column_config(): array;

	/**
	 * Generate SQL for data insertion.
	 *
	 * @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration_for_ids()` method.
	 *
	 * @return string Generated queries for insertion for this batch, would be of the form:
	 * INSERT IGNORE INTO $table_name ($columns) values
	 *  ($value for row 1)
	 *  ($value for row 2)
	 * ...
	 */
	private function generate_insert_sql_for_batch( array $batch ): string {
		$table = $this->schema_config['destination']['table_name'];

		list( $value_sql, $column_sql ) = $this->generate_column_clauses( array_merge( $this->core_column_mapping, $this->meta_column_mapping ), $batch );

		return "INSERT IGNORE INTO $table (`$column_sql`) VALUES $value_sql;"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, -- $insert_query is hardcoded, $value_sql is already escaped.
	}

	/**
	 * Generate SQL for data updating.
	 *
	 * @param array $batch Data to generate queries for. Will be `data` array returned by fetch_data_for_migration_for_ids() method.
	 *
	 * @param array $entity_row_mapping Maps rows to update data with their original IDs. Will be returned by `generate_update_sql_for_batch`.
	 *
	 * @return string Generated queries for batch update. Would be of the form:
	 * INSERT INTO $table ( $columns ) VALUES
	 *  ($value for row 1)
	 *  ($valye for row 2)
	 * ...
	 * ON DUPLICATE KEY UPDATE
	 * $column1 = VALUES($column1)
	 * $column2 = VALUES($column2)
	 * ...
	 */
	private function generate_update_sql_for_batch( array $batch, array $entity_row_mapping ): string {
		$table = $this->schema_config['destination']['table_name'];

		$destination_primary_id_schema = $this->get_destination_table_primary_id_schema();
		foreach ( $batch as $entity_id => $row ) {
			$batch[ $entity_id ][ $destination_primary_id_schema['destination_primary_key']['destination'] ] = $entity_row_mapping[ $entity_id ]->destination_id;
		}

		list( $value_sql, $column_sql, $columns ) = $this->generate_column_clauses(
			array_merge( $destination_primary_id_schema, $this->core_column_mapping, $this->meta_column_mapping ),
			$batch
		);

		$duplicate_update_key_statement = MigrationHelper::generate_on_duplicate_statement_clause( $columns );

		return "INSERT INTO $table (`$column_sql`) VALUES $value_sql $duplicate_update_key_statement;";
	}

	/**
	 * Generate schema for primary ID column of destination table.
	 *
	 * @return array[] Schema for primary ID column.
	 */
	private function get_destination_table_primary_id_schema(): array {
		return array(
			'destination_primary_key' => array(
				'destination' => $this->schema_config['destination']['primary_key'],
				'type'        => $this->schema_config['destination']['primary_key_type'],
			),
		);
	}

	/**
	 * Generate values and columns clauses to be used in INSERT and INSERT..ON DUPLICATE KEY UPDATE statements.
	 *
	 * @param array $columns_schema Columns config for destination table.
	 * @param array $batch Actual data to migrate as returned by `data` in `fetch_data_for_migration_for_ids` method.
	 *
	 * @return array SQL clause for values, columns placeholders, and columns.
	 */
	private function generate_column_clauses( array $columns_schema, array $batch ): array {
		global $wpdb;

		$columns      = array();
		$placeholders = array();
		foreach ( $columns_schema as $prev_column => $schema ) {
			if ( in_array( $schema['destination'], $columns, true ) ) {
				continue;
			}
			$columns[]      = $schema['destination'];
			$placeholders[] = MigrationHelper::get_wpdb_placeholder_for_type( $schema['type'] );
		}
		$placeholders = "'" . implode( "', '", $placeholders ) . "'";

		$values = array();
		foreach ( array_values( $batch ) as $row ) {
			$query_params = array();
			foreach ( $columns as $column ) {
				$query_params[] = $row[ $column ] ?? null;
			}
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $placeholders can only contain combination of placeholders described in MigrationHelper::get_wpdb_placeholder_for_type
			$value_string = '(' . $wpdb->prepare( $placeholders, $query_params ) . ')';
			$values[]     = $value_string;
		}

		$value_sql = implode( ',', $values );

		$column_sql = implode( '`, `', $columns );

		return array( $value_sql, $column_sql, $columns );
	}

	/**
	 * Migrate a batch of entities from the posts table to the corresponding table.
	 *
	 * @param array $entity_ids Ids of entities to migrate.
	 *
	 * @return void
	 */
	protected function process_migration_batch_for_ids_core( array $entity_ids ): void {
		$data = $this->fetch_data_for_migration_for_ids( $entity_ids );

		foreach ( $data['errors'] as $entity_id => $errors ) {
			foreach ( $errors as $column_name => $error_message ) {
				$this->add_error( "Error importing data for post with id $entity_id: column $column_name: $error_message" );
			}
		}

		if ( count( $data['data'] ) === 0 ) {
			return;
		}

		$entity_ids       = array_keys( $data['data'] );
		$existing_records = $this->get_already_existing_records( $entity_ids );

		$to_insert = array_diff_key( $data['data'], $existing_records );
		$this->process_insert_batch( $to_insert );

		$existing_records = array_filter(
			$existing_records,
			function( $record_data ) {
				return '1' === $record_data->modified;
			}
		);
		$to_update        = array_intersect_key( $data['data'], $existing_records );
		$this->process_update_batch( $to_update, $existing_records );
	}

	/**
	 * Process batch for insertion into destination table.
	 *
	 * @param array $batch Data to insert, will be of the form as returned by `data` in `fetch_data_for_migration_for_ids`.
	 */
	private function process_insert_batch( array $batch ): void {
		if ( 0 === count( $batch ) ) {
			return;
		}

		$queries = $this->generate_insert_sql_for_batch( $batch );
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Queries should already be prepared.
		$processed_rows_count = $this->db_query( $queries );
		$this->maybe_add_insert_or_update_error( 'insert', $processed_rows_count );
	}

	/**
	 * Process batch for update into destination table.
	 *
	 * @param array $batch Data to insert, will be of the form as returned by `data` in `fetch_data_for_migration_for_ids`.
	 * @param array $ids_mapping Maps rows to update data with their original IDs.
	 */
	private function process_update_batch( array $batch, array $ids_mapping ): void {
		if ( 0 === count( $batch ) ) {
			return;
		}

		$queries = $this->generate_update_sql_for_batch( $batch, $ids_mapping );
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Queries should already be prepared.
		$processed_rows_count = $this->db_query( $queries ) / 2;
		$this->maybe_add_insert_or_update_error( 'update', $processed_rows_count );
	}


	/**
	 * Fetch data for migration.
	 *
	 * @param array $entity_ids Entity IDs to fetch data for.
	 *
	 * @return array[] Data along with errors (if any), will of the form:
	 * array(
	 *  'data' => array(
	 *      'id_1' => array( 'column1' => value1, 'column2' => value2, ...),
	 *      ...,
	 *   ),
	 *  'errors' => array(
	 *      'id_1' => array( 'column1' => error1, 'column2' => value2, ...),
	 *      ...,
	 * )
	 */
	private function fetch_data_for_migration_for_ids( array $entity_ids ): array {
		if ( empty( $entity_ids ) ) {
			return array(
				'data'   => array(),
				'errors' => array(),
			);
		}

		$entity_table_query = $this->build_entity_table_query( $entity_ids );
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_entity_table_query is already prepared.
		$entity_data = $this->db_get_results( $entity_table_query );
		if ( empty( $entity_data ) ) {
			return array(
				'data'   => array(),
				'errors' => array(),
			);
		}
		$entity_meta_rel_ids = array_column( $entity_data, 'entity_meta_rel_id' );

		$meta_table_query = $this->build_meta_data_query( $entity_meta_rel_ids );
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_meta_data_query is already prepared.
		$meta_data = $this->db_get_results( $meta_table_query );

		return $this->process_and_sanitize_data( $entity_data, $meta_data );
	}

	/**
	 * Fetch id mappings for records that are already inserted in the destination table.
	 *
	 * @param array $entity_ids List of entity IDs to verify.
	 *
	 * @return array Already migrated entities, would be of the form
	 * array(
	 *      '$source_id1' => array(
	 *          'source_id' => $source_id1,
	 *          'destination_id' => $destination_id1
	 *          'modified' => 0 if it can be determined that the row doesn't need update, 1 otherwise
	 *      ),
	 *      ...
	 * )
	 */
	protected function get_already_existing_records( array $entity_ids ): array {
		global $wpdb;

		$source_table                   = $this->schema_config['source']['entity']['table_name'];
		$source_destination_join_column = $this->schema_config['source']['entity']['destination_rel_column'];
		$source_primary_key_column      = $this->schema_config['source']['entity']['primary_key'];

		$destination_table              = $this->schema_config['destination']['table_name'];
		$destination_source_join_column = $this->schema_config['destination']['source_rel_column'];
		$destination_primary_key_column = $this->schema_config['destination']['primary_key'];

		$entity_id_placeholder = implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) );

		// Additional SQL to check if the row needs update according to the column mapping.
		// The IFNULL and CHAR(0) "hack" is needed because NULLs can't be directly compared in SQL.
		$modified_selector   = array();
		$core_column_mapping = array_filter(
			$this->core_column_mapping,
			function( $mapping ) {
				return ! isset( $mapping['select_clause'] );
			}
		);
		foreach ( $core_column_mapping as $column_name => $mapping ) {
			if ( $column_name === $source_primary_key_column ) {
				continue;
			}
			$modified_selector[] =
				"IFNULL(source.$column_name,CHAR(0)) != IFNULL(destination.{$mapping['destination']},CHAR(0))"
				. ( 'string' === $mapping['type'] ? ' COLLATE ' . $wpdb->collate : '' );
		}

		if ( empty( $modified_selector ) ) {
			$modified_selector = ', 1 AS modified';
		} else {
			$modified_selector = trim( implode( ' OR ', $modified_selector ) );
			$modified_selector = ", if( $modified_selector, 1, 0 ) AS modified";
		}

		$additional_where = $this->get_additional_where_clause_for_get_data_to_insert_or_update( $entity_ids );

		$already_migrated_entity_ids = $this->db_get_results(
			$wpdb->prepare(
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded.
				"
SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id $modified_selector
FROM `$destination_table` destination
JOIN `$source_table` source ON source.`$source_destination_join_column` = destination.`$destination_source_join_column`
WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) $additional_where
",
				$entity_ids
			)
		// phpcs:enable
		);

		return array_column( $already_migrated_entity_ids, null, 'source_id' );
	}

	/**
	 * Get additional string to be appended to the WHERE clause of the SQL query used by get_data_to_insert_or_update.
	 *
	 * @param array $entity_ids The ids of the entities being inserted or updated.
	 * @return string Additional string for the WHERE clause, must either be empty or start with "AND" or "OR".
	 */
	protected function get_additional_where_clause_for_get_data_to_insert_or_update( array $entity_ids ): string {
		return '';
	}

	/**
	 * Helper method to build query used to fetch data from core source table.
	 *
	 * @param array $entity_ids List of entity IDs to fetch.
	 *
	 * @return string Query that can be used to fetch data.
	 */
	private function build_entity_table_query( array $entity_ids ): string {
		global $wpdb;

		$source_entity_table       = $this->schema_config['source']['entity']['table_name'];
		$source_meta_rel_id_column = "`$source_entity_table`.`{$this->schema_config['source']['entity']['meta_rel_column']}`";
		$source_primary_key_column = "`$source_entity_table`.`{$this->schema_config['source']['entity']['primary_key']}`";

		$where_clause = "$source_primary_key_column IN (" . implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) ) . ')';
		$entity_keys  = array();
		foreach ( $this->core_column_mapping as $column_name => $column_schema ) {
			if ( isset( $column_schema['select_clause'] ) ) {
				$select_clause = $column_schema['select_clause'];
				$entity_keys[] = "$select_clause AS $column_name";
			} else {
				$entity_keys[] = "$source_entity_table.$column_name";
			}
		}
		$entity_column_string = implode( ', ', $entity_keys );
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $source_meta_rel_id_column, $source_destination_rel_id_column etc is escaped for backticks. $where clause and $order_by should already be escaped.
		$query = $wpdb->prepare(
			"
SELECT
	$source_meta_rel_id_column as entity_meta_rel_id,
    $source_primary_key_column as primary_key_id,
	$entity_column_string
FROM `$source_entity_table`
WHERE $where_clause;
",
			$entity_ids
		);

		// phpcs:enable

		return $query;
	}

	/**
	 * Helper method to build query that will be used to fetch data from source meta table.
	 *
	 * @param array $entity_ids List of IDs to fetch metadata for.
	 *
	 * @return string Query for fetching meta data.
	 */
	private function build_meta_data_query( array $entity_ids ): string {
		global $wpdb;

		$meta_table                = $this->schema_config['source']['meta']['table_name'];
		$meta_keys                 = array_keys( $this->meta_column_mapping );
		$meta_key_column           = $this->schema_config['source']['meta']['meta_key_column'];
		$meta_value_column         = $this->schema_config['source']['meta']['meta_value_column'];
		$meta_table_relational_key = $this->schema_config['source']['meta']['entity_id_column'];

		$meta_column_string = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) );
		$entity_id_string   = implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $meta_table_relational_key, $meta_key_column, $meta_value_column and $meta_table is escaped for backticks. $entity_id_string and $meta_column_string are placeholders.
		$query = $wpdb->prepare(
			"
SELECT `$meta_table_relational_key` as entity_id, `$meta_key_column` as meta_key, `$meta_value_column` as meta_value
FROM `$meta_table`
WHERE
	`$meta_table_relational_key` IN ( $entity_id_string )
	AND `$meta_key_column` IN ( $meta_column_string );
",
			array_merge(
				$entity_ids,
				$meta_keys
			)
		);

		// phpcs:enable

		return $query;
	}

	/**
	 * Helper function to validate and combine data before we try to insert.
	 *
	 * @param array $entity_data Data from source table.
	 * @param array $meta_data Data from meta table.
	 *
	 * @return array[] Validated and combined data with errors.
	 */
	private function process_and_sanitize_data( array $entity_data, array $meta_data ): array {
		$sanitized_entity_data = array();
		$error_records         = array();
		$this->process_and_sanitize_entity_data( $sanitized_entity_data, $error_records, $entity_data );
		$this->processs_and_sanitize_meta_data( $sanitized_entity_data, $error_records, $meta_data );

		return array(
			'data'   => $sanitized_entity_data,
			'errors' => $error_records,
		);
	}

	/**
	 * Helper method to sanitize core source table.
	 *
	 * @param array $sanitized_entity_data Array containing sanitized data for insertion.
	 * @param array $error_records Error records.
	 * @param array $entity_data Original source data.
	 */
	private function process_and_sanitize_entity_data( array &$sanitized_entity_data, array &$error_records, array $entity_data ): void {
		foreach ( $entity_data as $entity ) {
			$row_data = array();
			foreach ( $this->core_column_mapping as $column_name => $schema ) {
				$custom_table_column_name = $schema['destination'] ?? $column_name;
				$value                    = $entity->$column_name;
				$value                    = $this->validate_data( $value, $schema['type'] );
				if ( is_wp_error( $value ) ) {
					$error_records[ $entity->primary_key_id ][ $custom_table_column_name ] = $value->get_error_code();
				} else {
					$row_data[ $custom_table_column_name ] = $value;
				}
			}
			$sanitized_entity_data[ $entity->entity_meta_rel_id ] = $row_data;
		}
	}

	/**
	 * Helper method to sanitize soure meta data.
	 *
	 * @param array $sanitized_entity_data Array containing sanitized data for insertion.
	 * @param array $error_records Error records.
	 * @param array $meta_data Original source data.
	 */
	private function processs_and_sanitize_meta_data( array &$sanitized_entity_data, array &$error_records, array $meta_data ): void {
		foreach ( $meta_data as $datum ) {
			$column_schema = $this->meta_column_mapping[ $datum->meta_key ];
			$value         = $this->validate_data( $datum->meta_value, $column_schema['type'] );
			if ( is_wp_error( $value ) ) {
				$error_records[ $datum->entity_id ][ $column_schema['destination'] ] = "{$value->get_error_code()}: {$value->get_error_message()}";
			} else {
				$sanitized_entity_data[ $datum->entity_id ][ $column_schema['destination'] ] = $value;
			}
		}
	}

	/**
	 * Validate and transform data so that we catch as many errors as possible before inserting.
	 *
	 * @param mixed  $value Actual data value.
	 * @param string $type Type of data, could be decimal, int, date, string.
	 *
	 * @return float|int|mixed|string|\WP_Error
	 */
	private function validate_data( $value, string $type ) {
		switch ( $type ) {
			case 'decimal':
				$value = wc_format_decimal( $value, false, true );
				break;
			case 'int':
				$value = (int) $value;
				break;
			case 'bool':
				$value = wc_string_to_bool( $value );
				break;
			case 'date':
				try {
					if ( '' === $value ) {
						$value = null;
					} else {
						$value = ( new \DateTime( $value ) )->format( 'Y-m-d H:i:s' );
					}
				} catch ( \Exception $e ) {
					return new \WP_Error( $e->getMessage() );
				}
				break;
			case 'date_epoch':
				try {
					if ( '' === $value ) {
						$value = null;
					} else {
						$value = ( new \DateTime( "@$value" ) )->format( 'Y-m-d H:i:s' );
					}
				} catch ( \Exception $e ) {
					return new \WP_Error( $e->getMessage() );
				}
				break;
		}

		return $value;
	}

	/**
	 * Verify whether data was migrated properly for given IDs.
	 *
	 * @param array $source_ids List of source IDs.
	 *
	 * @return array List of IDs along with columns that failed to migrate.
	 */
	public function verify_migrated_data( array $source_ids ) : array {
		global $wpdb;
		$query = $this->build_verification_query( $source_ids );
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query should already be prepared.
		$results = $wpdb->get_results( $query, ARRAY_A );

		return $this->verify_data( $results );
	}

	/**
	 * Generate query to fetch data from both source and destination tables. Use the results in `verify_data` to verify if data was migrated properly.
	 *
	 * @param array $source_ids Array of IDs in source table.
	 *
	 * @return string SELECT statement.
	 */
	protected function build_verification_query( $source_ids ) {
		$source_table                  = $this->schema_config['source']['entity']['table_name'];
		$meta_table                    = $this->schema_config['source']['meta']['table_name'];
		$destination_table             = $this->schema_config['destination']['table_name'];
		$meta_entity_id_column         = $this->schema_config['source']['meta']['entity_id_column'];
		$meta_key_column               = $this->schema_config['source']['meta']['meta_key_column'];
		$meta_value_column             = $this->schema_config['source']['meta']['meta_value_column'];
		$destination_source_rel_column = $this->schema_config['destination']['source_rel_column'];
		$source_destination_rel_column = $this->schema_config['source']['entity']['destination_rel_column'];
		$source_meta_rel_column        = $this->schema_config['source']['entity']['meta_rel_column'];

		$source_destination_join_clause = "$destination_table ON $destination_table.$destination_source_rel_column = $source_table.$source_destination_rel_column";

		$meta_select_clauses        = array();
		$meta_join_clauses          = array();
		$source_select_clauses      = array();
		$destination_select_clauses = array();

		foreach ( $this->core_column_mapping as $column_name => $schema ) {
			$source_select_column         = isset( $schema['select_clause'] ) ? $schema['select_clause'] : "$source_table.$column_name";
			$source_select_clauses[]      = "$source_select_column as {$source_table}_{$column_name}";
			$destination_select_clauses[] = "$destination_table.{$schema['destination']} as {$destination_table}_{$schema['destination']}";
		}

		foreach ( $this->meta_column_mapping as $meta_key => $schema ) {
			$meta_table_alias             = "meta_source_{$schema['destination']}";
			$meta_select_clauses[]        = "$meta_table_alias.$meta_value_column AS $meta_table_alias";
			$meta_join_clauses[]          = "
$meta_table $meta_table_alias ON
	$meta_table_alias.$meta_entity_id_column = $source_table.$source_meta_rel_column AND
	$meta_table_alias.$meta_key_column = '$meta_key'
";
			$destination_select_clauses[] = "$destination_table.{$schema['destination']} as {$destination_table}_{$schema['destination']}";
		}

		$select_clause = implode( ', ', array_merge( $source_select_clauses, $meta_select_clauses, $destination_select_clauses ) );

		$meta_join_clause = implode( ' LEFT JOIN ', $meta_join_clauses );

		$where_clause = $this->get_where_clause_for_verification( $source_ids );

		return "
SELECT $select_clause
FROM $source_table
    LEFT JOIN $source_destination_join_clause
    LEFT JOIN $meta_join_clause
WHERE $where_clause
";
	}

	/**
	 * Helper function to generate where clause for fetching data for verification.
	 *
	 * @param array $source_ids Array of IDs from source table.
	 *
	 * @return string WHERE clause.
	 */
	protected function get_where_clause_for_verification( $source_ids ) {
		global $wpdb;
		$source_primary_id_column = $this->schema_config['source']['entity']['primary_key'];
		$source_table             = $this->schema_config['source']['entity']['table_name'];
		$source_ids_placeholder   = implode( ', ', array_fill( 0, count( $source_ids ), '%d' ) );

		return $wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
			"$source_table.$source_primary_id_column IN ($source_ids_placeholder)",
			$source_ids
		);
	}

	/**
	 * Verify data from both source and destination tables and check if they were migrated properly.
	 *
	 * @param array $collected_data Collected data in array format, should be in same structure as returned from query in `$this->build_verification_query`.
	 *
	 * @return array Array of failed IDs if any, along with columns/meta_key names.
	 */
	protected function verify_data( $collected_data ) {
		$failed_ids = array();
		foreach ( $collected_data as $row ) {
			$failed_ids = $this->verify_entity_columns( $row, $failed_ids );
			$failed_ids = $this->verify_meta_columns( $row, $failed_ids );
		}

		return $failed_ids;
	}

	/**
	 * Helper method to verify and compare core columns.
	 *
	 * @param array $row        Both migrated and source data for a single row.
	 * @param array $failed_ids Array of failed IDs.
	 *
	 * @return array Array of failed IDs if any, along with columns/meta_key names.
	 */
	private function verify_entity_columns( $row, $failed_ids ) {
		$primary_key_column = "{$this->schema_config['source']['entity']['table_name']}_{$this->schema_config['source']['entity']['primary_key']}";
		foreach ( $this->core_column_mapping as $column_name => $schema ) {
			$source_alias      = "{$this->schema_config['source']['entity']['table_name']}_$column_name";
			$destination_alias = "{$this->schema_config['destination']['table_name']}_{$schema['destination']}";
			$row               = $this->pre_process_row( $row, $schema, $source_alias, $destination_alias );
			if ( $row[ $source_alias ] !== $row[ $destination_alias ] ) {
				if ( ! isset( $failed_ids[ $row[ $primary_key_column ] ] ) ) {
					$failed_ids[ $row[ $primary_key_column ] ] = array();
				}
				$failed_ids[ $row[ $primary_key_column ] ][] = array(
					'column'         => $column_name,
					'original_value' => $row[ $source_alias ],
					'new_value'      => $row[ $destination_alias ],
				);
			}
		}

		return $failed_ids;
	}

	/**
	 * Helper method to verify meta columns.
	 *
	 * @param array $row        Both migrated and source data for a single row.
	 * @param array $failed_ids Array of failed IDs.
	 *
	 * @return array Array of failed IDs if any, along with columns/meta_key names.
	 */
	private function verify_meta_columns( $row, $failed_ids ) {
		$primary_key_column = "{$this->schema_config['source']['entity']['table_name']}_{$this->schema_config['source']['entity']['primary_key']}";
		foreach ( $this->meta_column_mapping as $meta_key => $schema ) {
			$meta_alias        = "meta_source_{$schema['destination']}";
			$destination_alias = "{$this->schema_config['destination']['table_name']}_{$schema['destination']}";
			$row               = $this->pre_process_row( $row, $schema, $meta_alias, $destination_alias );
			if ( $row[ $meta_alias ] !== $row[ $destination_alias ] ) {
				if ( ! isset( $failed_ids[ $row[ $primary_key_column ] ] ) ) {
					$failed_ids[ $row[ $primary_key_column ] ] = array();
				}
				$failed_ids[ $row[ $primary_key_column ] ][] = array(
					'column'         => $meta_key,
					'original_value' => $row[ $meta_alias ],
					'new_value'      => $row[ $destination_alias ],
				);
			}
		}

		return $failed_ids;
	}

	/**
	 * Helper method to pre-process rows to make sure we parse the correct type.
	 *
	 * @param array  $row Both migrated and source data for a single row.
	 * @param array  $schema Column schema.
	 * @param string $alias Name of source column.
	 * @param string $destination_alias Name of destination column.
	 *
	 * @return array Processed row.
	 */
	private function pre_process_row( $row, $schema, $alias, $destination_alias ) {
		if ( in_array( $schema['type'], array( 'int', 'decimal' ), true ) ) {
			if ( '' === $row[ $alias ] || null === $row[ $alias ] ) {
				$row[ $alias ] = 0; // $wpdb->prepare forces empty values to 0.
			}
			$row[ $alias ]             = wc_format_decimal( $row[ $alias ], false, true );
			$row[ $destination_alias ] = wc_format_decimal( $row[ $destination_alias ], false, true );
		}
		if ( 'bool' === $schema['type'] ) {
			$row[ $alias ]             = wc_string_to_bool( $row[ $alias ] );
			$row[ $destination_alias ] = wc_string_to_bool( $row[ $destination_alias ] );
		}
		if ( 'date_epoch' === $schema['type'] ) {
			if ( '' === $row[ $alias ] || null === $row[ $alias ] ) {
				$row[ $alias ] = null;
			} else {
				$row[ $alias ] = ( new \DateTime( "@{$row[ $alias ]}" ) )->format( 'Y-m-d H:i:s' );
			}
			if ( '0000-00-00 00:00:00' === $row[ $destination_alias ] ) {
				$row[ $destination_alias ] = null;
			}
		}
		if ( is_null( $row[ $alias ] ) ) {
			$row[ $alias ] = $this->get_type_defaults( $schema['type'] );
		}

		return $row;
	}

	/**
	 * Helper method to get default value of a type.
	 *
	 * @param string $type Type.
	 *
	 * @return mixed Default value.
	 */
	private function get_type_defaults( $type ) {
		switch ( $type ) {
			case 'float':
			case 'int':
				return 0;
			case 'string':
				return '';
		}
	}
}
Database/Migrations/MetaToMetaTableMigrator.php000064400000034375152333011040015556 0ustar00<?php
/**
 * Generic Migration class to move any meta data associated to an entity, to a different meta table associated with a custom entity table.
 */

namespace Automattic\WooCommerce\Database\Migrations;

/**
 * Base class for implementing migrations from the standard WordPress meta table
 * to custom meta (key-value pairs) tables.
 *
 * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
 */
abstract class MetaToMetaTableMigrator extends TableMigrator {

	/**
	 * Schema config, see __construct for more details.
	 *
	 * @var array
	 */
	private $schema_config;

	/**
	 * Returns config for the migration.
	 *
	 * @return array Meta config, must be in following format:
	 * array(
	 *  'source'      => array(
	 *      'meta'          => array(
	 *          'table_name'        => source_meta_table_name,
	 *          'entity_id_column'  => entity_id column name in source meta table,
	 *          'meta_key_column'   => meta_key column',
	 *          'meta_value_column' => meta_value column',
	 *      ),
	 *      'entity' => array(
	 *          'table_name'       => entity table name for the meta table,
	 *          'source_id_column' => column name in entity table which maps to meta table,
	 *          'id_column'        => id column in entity table,
	 *      ),
	 *      'excluded_keys' => array of keys to exclude,
	 *  ),
	 *  'destination' => array(
	 *      'meta'   => array(
	 *          'table_name'        => destination meta table name,
	 *          'entity_id_column'  => entity_id column in meta table,
	 *          'meta_key_column'   => meta key column,
	 *          'meta_value_column' => meta_value column,
	 *          'entity_id_type'    => data type of entity id,
	 *          'meta_id_column'    => id column in meta table,
	 *      ),
	 *  ),
	 * )
	 */
	abstract protected function get_meta_config(): array;

	/**
	 * MetaToMetaTableMigrator constructor.
	 */
	public function __construct() {
		$this->schema_config = $this->get_meta_config();
	}

	/**
	 * Migrate a batch of entities from the posts table to the corresponding table.
	 *
	 * @param array $entity_ids Ids of entities ro migrate.
	 */
	protected function process_migration_batch_for_ids_core( array $entity_ids ): void {
		$to_migrate = $this->fetch_data_for_migration_for_ids( $entity_ids );
		if ( empty( $to_migrate ) ) {
			return;
		}

		$already_migrated = $this->get_already_migrated_records( array_keys( $to_migrate ) );

		$data      = $this->classify_update_insert_records( $to_migrate, $already_migrated );
		$to_insert = $data[0];
		$to_update = $data[1];

		if ( ! empty( $to_insert ) ) {
			$insert_queries       = $this->generate_insert_sql_for_batch( $to_insert );
			$processed_rows_count = $this->db_query( $insert_queries );
			$this->maybe_add_insert_or_update_error( 'insert', $processed_rows_count );
		}

		if ( ! empty( $to_update ) ) {
			$update_queries       = $this->generate_update_sql_for_batch( $to_update );
			$processed_rows_count = $this->db_query( $update_queries );
			$this->maybe_add_insert_or_update_error( 'update', $processed_rows_count );
		}
	}

	/**
	 * Generate update SQL for given batch.
	 *
	 * @param array $batch List of data to generate update SQL for. Should be in same format as output of $this->fetch_data_for_migration_for_ids.
	 *
	 * @return string Query to update batch records.
	 */
	private function generate_update_sql_for_batch( array $batch ): string {
		global $wpdb;

		$table             = $this->schema_config['destination']['meta']['table_name'];
		$meta_id_column    = $this->schema_config['destination']['meta']['meta_id_column'];
		$meta_key_column   = $this->schema_config['destination']['meta']['meta_key_column'];
		$meta_value_column = $this->schema_config['destination']['meta']['meta_value_column'];
		$entity_id_column  = $this->schema_config['destination']['meta']['entity_id_column'];
		$columns           = array( $meta_id_column, $entity_id_column, $meta_key_column, $meta_value_column );
		$columns_sql       = implode( '`, `', $columns );

		$entity_id_column_placeholder = MigrationHelper::get_wpdb_placeholder_for_type( $this->schema_config['destination']['meta']['entity_id_type'] );
		$placeholder_string           = "%d, $entity_id_column_placeholder, %s, %s";
		$values                       = array();
		foreach ( $batch as $entity_id => $rows ) {
			foreach ( $rows as $meta_key => $meta_details ) {

				// phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders
				$values[] = $wpdb->prepare(
					"( $placeholder_string )",
					array( $meta_details['id'], $entity_id, $meta_key, $meta_details['meta_value'] )
				);
				// phpcs:enable
			}
		}
		$value_sql = implode( ',', $values );

		$on_duplicate_key_clause = MigrationHelper::generate_on_duplicate_statement_clause( $columns );

		return "INSERT INTO $table ( `$columns_sql` ) VALUES $value_sql $on_duplicate_key_clause";
	}

	/**
	 * Generate insert sql queries for batches.
	 *
	 * @param array $batch Data to generate queries for.
	 *
	 * @return string Insert SQL query.
	 */
	private function generate_insert_sql_for_batch( array $batch ): string {
		global $wpdb;

		$table             = $this->schema_config['destination']['meta']['table_name'];
		$meta_key_column   = $this->schema_config['destination']['meta']['meta_key_column'];
		$meta_value_column = $this->schema_config['destination']['meta']['meta_value_column'];
		$entity_id_column  = $this->schema_config['destination']['meta']['entity_id_column'];
		$column_sql        = "(`$entity_id_column`, `$meta_key_column`, `$meta_value_column`)";

		$entity_id_column_placeholder = MigrationHelper::get_wpdb_placeholder_for_type( $this->schema_config['destination']['meta']['entity_id_type'] );
		$placeholder_string           = "$entity_id_column_placeholder, %s, %s";
		$values                       = array();
		foreach ( $batch as $entity_id => $rows ) {
			foreach ( $rows as $meta_key => $meta_values ) {
				foreach ( $meta_values as $meta_value ) {
					$query_params = array(
						$entity_id,
						$meta_key,
						$meta_value,
					);
					// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders
					$value_sql = $wpdb->prepare( "$placeholder_string", $query_params );
					$values[]  = $value_sql;
				}
			}
		}

		$values_sql = implode( '), (', $values );

		return "INSERT IGNORE INTO $table $column_sql VALUES ($values_sql)";
	}

	/**
	 * Fetch data for migration.
	 *
	 * @param array $entity_ids Array of IDs to fetch data for.
	 *
	 * @return array[] Data, will of the form:
	 * array(
	 *   'id_1' => array( 'column1' => array( value1_1, value1_2...), 'column2' => array(value2_1, value2_2...), ...),
	 *   ...,
	 * )
	 */
	private function fetch_data_for_migration_for_ids( array $entity_ids ): array {
		if ( empty( $entity_ids ) ) {
			return array();
		}

		$meta_query = $this->build_meta_table_query( $entity_ids );

		$meta_data_rows = $this->db_get_results( $meta_query );
		if ( empty( $meta_data_rows ) ) {
			return array();
		}

		foreach ( $meta_data_rows as $migrate_row ) {
			if ( ! isset( $to_migrate[ $migrate_row->entity_id ] ) ) {
				$to_migrate[ $migrate_row->entity_id ] = array();
			}

			if ( ! isset( $to_migrate[ $migrate_row->entity_id ][ $migrate_row->meta_key ] ) ) {
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
				$to_migrate[ $migrate_row->entity_id ][ $migrate_row->meta_key ] = array();
			}

			$to_migrate[ $migrate_row->entity_id ][ $migrate_row->meta_key ][] = $migrate_row->meta_value;
		}

		return $to_migrate;
	}

	/**
	 * Helper method to get already migrated records. Will be used to find prevent migration of already migrated records.
	 *
	 * @param array $entity_ids List of entity ids to check for.
	 *
	 * @return array Already migrated records.
	 */
	private function get_already_migrated_records( array $entity_ids ): array {
		global $wpdb;

		$destination_table_name        = $this->schema_config['destination']['meta']['table_name'];
		$destination_id_column         = $this->schema_config['destination']['meta']['meta_id_column'];
		$destination_entity_id_column  = $this->schema_config['destination']['meta']['entity_id_column'];
		$destination_meta_key_column   = $this->schema_config['destination']['meta']['meta_key_column'];
		$destination_meta_value_column = $this->schema_config['destination']['meta']['meta_value_column'];

		$entity_id_type_placeholder = MigrationHelper::get_wpdb_placeholder_for_type( $this->schema_config['destination']['meta']['entity_id_type'] );
		$entity_ids_placeholder     = implode( ',', array_fill( 0, count( $entity_ids ), $entity_id_type_placeholder ) );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
		$data_already_migrated = $this->db_get_results(
			$wpdb->prepare(
				"
SELECT
	   $destination_id_column meta_id,
       $destination_entity_id_column entity_id,
       $destination_meta_key_column meta_key,
       $destination_meta_value_column meta_value
FROM $destination_table_name destination
WHERE destination.$destination_entity_id_column in ( $entity_ids_placeholder ) ORDER BY destination.$destination_entity_id_column
",
				$entity_ids
			)
		);
		// phpcs:enable

		$already_migrated = array();

		foreach ( $data_already_migrated as $migrate_row ) {
			if ( ! isset( $already_migrated[ $migrate_row->entity_id ] ) ) {
				$already_migrated[ $migrate_row->entity_id ] = array();
			}

			// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
			if ( ! isset( $already_migrated[ $migrate_row->entity_id ][ $migrate_row->meta_key ] ) ) {
				$already_migrated[ $migrate_row->entity_id ][ $migrate_row->meta_key ] = array();
			}

			$already_migrated[ $migrate_row->entity_id ][ $migrate_row->meta_key ][] = array(
				'id'         => $migrate_row->meta_id,
				'meta_value' => $migrate_row->meta_value,
			);
			// phpcs:enable
		}

		return $already_migrated;
	}

	/**
	 * Classify each record on whether to migrate or update.
	 *
	 * @param array $to_migrate Records to migrate.
	 * @param array $already_migrated Records already migrated.
	 *
	 * @return array[] Returns two arrays, first for records to migrate, and second for records to upgrade.
	 */
	private function classify_update_insert_records( array $to_migrate, array $already_migrated ): array {
		$to_update = array();
		$to_insert = array();

		foreach ( $to_migrate as $entity_id => $rows ) {
			foreach ( $rows as $meta_key => $meta_values ) {
				// If there is no corresponding record in the destination table then insert.
				// If there is single value in both already migrated and current then update.
				// If there are multiple values in either already_migrated records or in to_migrate_records, then insert instead of updating.
				if ( ! isset( $already_migrated[ $entity_id ][ $meta_key ] ) ) {
					if ( ! isset( $to_insert[ $entity_id ] ) ) {
						$to_insert[ $entity_id ] = array();
					}
					$to_insert[ $entity_id ][ $meta_key ] = $meta_values;
				} else {
					if ( 1 === count( $meta_values ) && 1 === count( $already_migrated[ $entity_id ][ $meta_key ] ) ) {
						if ( $meta_values[0] === $already_migrated[ $entity_id ][ $meta_key ][0]['meta_value'] ) {
							continue;
						}
						if ( ! isset( $to_update[ $entity_id ] ) ) {
							$to_update[ $entity_id ] = array();
						}
						$to_update[ $entity_id ][ $meta_key ] = array(
							'id'         => $already_migrated[ $entity_id ][ $meta_key ][0]['id'],
							// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
							'meta_value' => $meta_values[0],
						);
						continue;
					}

					// There are multiple meta entries, let's find the unique entries and insert.
					$unique_meta_values = array_diff( $meta_values, array_column( $already_migrated[ $entity_id ][ $meta_key ], 'meta_value' ) );
					if ( 0 === count( $unique_meta_values ) ) {
						continue;
					}
					if ( ! isset( $to_insert[ $entity_id ] ) ) {
						$to_insert[ $entity_id ] = array();
					}
					$to_insert[ $entity_id ][ $meta_key ] = $unique_meta_values;
				}
			}
		}

		return array( $to_insert, $to_update );
	}

	/**
	 * Helper method to build query used to fetch data from source meta table.
	 *
	 * @param array $entity_ids List of entity IDs to build meta query for.
	 *
	 * @return string Query that can be used to fetch data.
	 */
	private function build_meta_table_query( array $entity_ids ): string {
		global $wpdb;
		$source_meta_table        = $this->schema_config['source']['meta']['table_name'];
		$source_meta_key_column   = $this->schema_config['source']['meta']['meta_key_column'];
		$source_meta_value_column = $this->schema_config['source']['meta']['meta_value_column'];
		$source_entity_id_column  = $this->schema_config['source']['meta']['entity_id_column'];
		$order_by                 = "source.$source_entity_id_column ASC";

		$where_clause = "source.`$source_entity_id_column` IN (" . implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ) . ')';

		$entity_table                  = $this->schema_config['source']['entity']['table_name'];
		$entity_id_column              = $this->schema_config['source']['entity']['id_column'];
		$entity_meta_id_mapping_column = $this->schema_config['source']['entity']['source_id_column'];

		if ( $this->schema_config['source']['excluded_keys'] ) {
			$key_placeholder = implode( ',', array_fill( 0, count( $this->schema_config['source']['excluded_keys'] ), '%s' ) );
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $source_meta_key_column is escaped for backticks, $key_placeholder is hardcoded.
			$exclude_clause = $wpdb->prepare( "source.$source_meta_key_column NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] );
			$where_clause   = "$where_clause AND $exclude_clause";
		}

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
		return $wpdb->prepare(
			"
SELECT
	source.`$source_entity_id_column` as source_entity_id,
	entity.`$entity_id_column` as entity_id,
	source.`$source_meta_key_column` as meta_key,
	source.`$source_meta_value_column` as meta_value
FROM `$source_meta_table` source
JOIN `$entity_table` entity ON entity.`$entity_meta_id_mapping_column` = source.`$source_entity_id_column`
WHERE $where_clause ORDER BY $order_by
",
			$entity_ids
		);
		// phpcs:enable
	}
}
Database/Migrations/CustomOrderTable/PostToOrderTableMigrator.php000064400000007117152333011040021212 0ustar00<?php
/**
 * Class for WPPost To order table migrator.
 */

namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;

use Automattic\WooCommerce\Database\Migrations\MetaToCustomTableMigrator;

/**
 * Helper class to migrate records from the WordPress post table
 * to the custom order table (and only that table - PostsToOrdersMigrationController
 * is used for fully migrating orders).
 */
class PostToOrderTableMigrator extends MetaToCustomTableMigrator {

	/**
	 * Get schema config for wp_posts and wc_order table.
	 *
	 * @return array Config.
	 */
	protected function get_schema_config(): array {
		global $wpdb;

		$this->table_names = array(
			'orders'    => $wpdb->prefix . 'wc_orders',
			'addresses' => $wpdb->prefix . 'wc_order_addresses',
			'op_data'   => $wpdb->prefix . 'wc_order_operational_data',
			'meta'      => $wpdb->prefix . 'wc_orders_meta',
		);

		return array(
			'source'      => array(
				'entity' => array(
					'table_name'             => $wpdb->posts,
					'meta_rel_column'        => 'ID',
					'destination_rel_column' => 'ID',
					'primary_key'            => 'ID',
				),
				'meta'   => array(
					'table_name'        => $wpdb->postmeta,
					'meta_key_column'   => 'meta_key',
					'meta_value_column' => 'meta_value',
					'entity_id_column'  => 'post_id',
				),
			),
			'destination' => array(
				'table_name'        => $this->table_names['orders'],
				'source_rel_column' => 'id',
				'primary_key'       => 'id',
				'primary_key_type'  => 'int',
			),
		);
	}

	/**
	 * Get columns config.
	 *
	 * @return \string[][] Config.
	 */
	protected function get_core_column_mapping(): array {
		return array(
			'ID'                => array(
				'type'        => 'int',
				'destination' => 'id',
			),
			'post_status'       => array(
				'type'        => 'string',
				'destination' => 'status',
			),
			'post_date_gmt'     => array(
				'type'        => 'date',
				'destination' => 'date_created_gmt',
			),
			'post_modified_gmt' => array(
				'type'        => 'date',
				'destination' => 'date_updated_gmt',
			),
			'post_parent'       => array(
				'type'        => 'int',
				'destination' => 'parent_order_id',
			),
			'post_type'         => array(
				'type'        => 'string',
				'destination' => 'type',
			),
			'post_excerpt'      => array(
				'type'        => 'string',
				'destination' => 'customer_note',
			),
		);
	}

	/**
	 * Get meta data config.
	 *
	 * @return \string[][] Config.
	 */
	public function get_meta_column_config(): array {
		return array(
			'_order_currency'       => array(
				'type'        => 'string',
				'destination' => 'currency',
			),
			'_order_tax'            => array(
				'type'        => 'decimal',
				'destination' => 'tax_amount',
			),
			'_order_total'          => array(
				'type'        => 'decimal',
				'destination' => 'total_amount',
			),
			'_customer_user'        => array(
				'type'        => 'int',
				'destination' => 'customer_id',
			),
			'_billing_email'        => array(
				'type'        => 'string',
				'destination' => 'billing_email',
			),
			'_payment_method'       => array(
				'type'        => 'string',
				'destination' => 'payment_method',
			),
			'_payment_method_title' => array(
				'type'        => 'string',
				'destination' => 'payment_method_title',
			),
			'_customer_ip_address'  => array(
				'type'        => 'string',
				'destination' => 'ip_address',
			),
			'_customer_user_agent'  => array(
				'type'        => 'string',
				'destination' => 'user_agent',
			),
			'_transaction_id'       => array(
				'type'        => 'string',
				'destination' => 'transaction_id',
			),
		);
	}
}
Database/Migrations/CustomOrderTable/CLIRunner.php000064400000040337152333011040016113 0ustar00<?php

namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable;

use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use WP_CLI;

/**
 * CLI tool for migrating order data to/from custom table.
 *
 * Credits https://github.com/liquidweb/woocommerce-custom-orders-table/blob/develop/includes/class-woocommerce-custom-orders-table-cli.php.
 *
 * Class CLIRunner
 */
class CLIRunner {

	/**
	 * CustomOrdersTableController instance.
	 *
	 * @var CustomOrdersTableController
	 */
	private $controller;

	/**
	 * DataSynchronizer instance.
	 *
	 * @var DataSynchronizer;
	 */
	private $synchronizer;

	/**
	 * PostsToOrdersMigrationController instance.
	 *
	 * @var PostsToOrdersMigrationController
	 */
	private $post_to_cot_migrator;

	/**
	 * Init method, invoked by DI container.
	 *
	 * @param CustomOrdersTableController      $controller Instance.
	 * @param DataSynchronizer                 $synchronizer Instance.
	 * @param PostsToOrdersMigrationController $posts_to_orders_migration_controller Instance.
	 *
	 * @internal
	 */
	final public function init( CustomOrdersTableController $controller, DataSynchronizer $synchronizer, PostsToOrdersMigrationController $posts_to_orders_migration_controller ) {
		$this->controller           = $controller;
		$this->synchronizer         = $synchronizer;
		$this->post_to_cot_migrator = $posts_to_orders_migration_controller;
	}

	/**
	 * Registers commands for CLI.
	 */
	public function register_commands() {
		WP_CLI::add_command( 'wc cot count_unmigrated', array( $this, 'count_unmigrated' ) );
		WP_CLI::add_command( 'wc cot migrate', array( $this, 'migrate' ) );
		WP_CLI::add_command( 'wc cot sync', array( $this, 'sync' ) );
		WP_CLI::add_command( 'wc cot verify_cot_data', array( $this, 'verify_cot_data' ) );
	}

	/**
	 * Check if the COT feature is enabled.
	 *
	 * @param bool $log Optionally log a error message.
	 *
	 * @return bool Whether the COT feature is enabled.
	 */
	private function is_enabled( $log = true ) : bool {
		if ( ! $this->controller->is_feature_visible() ) {
			if ( $log ) {
				WP_CLI::log(
					sprintf(
						// translators: %s - link to testing instructions webpage.
						__( 'Custom order table usage is not enabled. If you are testing, you can enable it by following the testing instructions in %s', 'woocommerce' ),
						'https://developer.woocommerce.com/' // TODO: Change the link when testing instructin page is live.
					)
				);
			}
		}

		return $this->controller->is_feature_visible();
	}

	/**
	 * Helper method to log warning that feature is not yet production ready.
	 */
	private function log_production_warning() {
		WP_CLI::log( __( 'This feature is not production ready yet. Make sure you are not running these commands in your production environment.', 'woocommerce' ) );
	}

	/**
	 * Count how many orders have yet to be migrated into the custom orders table.
	 *
	 * ## EXAMPLES
	 *
	 *     wp wc cot count_unmigrated
	 *
	 * @param array $args Positional arguments passed to the command.
	 *
	 * @param array $assoc_args Associative arguments (options) passed to the command.
	 *
	 * @return int The number of orders to be migrated.*
	 */
	public function count_unmigrated( $args = array(), $assoc_args = array() ) : int {
		if ( ! $this->is_enabled() ) {
			return 0;
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$order_count = $this->synchronizer->get_current_orders_pending_sync_count();

		$assoc_args = wp_parse_args(
			$assoc_args,
			array(
				'log' => true,
			)
		);
		if ( isset( $assoc_args['log'] ) && $assoc_args['log'] ) {
			WP_CLI::log(
				sprintf(
					/* Translators: %1$d is the number of orders to be synced. */
					_n(
						'There is %1$d order to be synced.',
						'There are %1$d orders to be synced.',
						$order_count,
						'woocommerce'
					),
					$order_count
				)
			);
		}

		return (int) $order_count;
	}

	/**
	 * Sync order data between the custom order tables and the core WordPress post tables.
	 *
	 * ## OPTIONS
	 *
	 * [--batch-size=<batch-size>]
	 * : The number of orders to process in each batch.
	 * ---
	 * default: 500
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     wp wc cot sync --batch-size=500
	 *
	 * @param array $args Positional arguments passed to the command.
	 * @param array $assoc_args Associative arguments (options) passed to the command.
	 */
	public function sync( $args = array(), $assoc_args = array() ) {
		$this->log_production_warning();
		if ( ! $this->is_enabled() ) {
			return;
		}

		$order_count = $this->count_unmigrated();

		// Abort if there are no orders to migrate.
		if ( ! $order_count ) {
			return WP_CLI::warning( __( 'There are no orders to sync, aborting.', 'woocommerce' ) );
		}

		$assoc_args  = wp_parse_args(
			$assoc_args,
			array(
				'batch-size' => 500,
			)
		);
		$batch_size  = ( (int) $assoc_args['batch-size'] ) === 0 ? 500 : (int) $assoc_args['batch-size'];
		$progress    = WP_CLI\Utils\make_progress_bar( 'Order Data Sync', $order_count / $batch_size );
		$processed   = 0;
		$batch_count = 1;
		$total_time  = 0;

		while ( $order_count > 0 ) {

			WP_CLI::debug(
				sprintf(
					/* Translators: %1$d is the batch number and %2$d is the batch size. */
					__( 'Beginning batch #%1$d (%2$d orders/batch).', 'woocommerce' ),
					$batch_count,
					$batch_size
				)
			);
			$batch_start_time = microtime( true );
			$order_ids        = $this->synchronizer->get_next_batch_to_process( $batch_size );
			if ( count( $order_ids ) ) {
				$this->synchronizer->process_batch( $order_ids );
			}
			$processed       += count( $order_ids );
			$batch_total_time = microtime( true ) - $batch_start_time;

			WP_CLI::debug(
				sprintf(
					// Translators: %1$d is the batch number, %2$d is the number of processed orders and %3$d is the execution time in seconds.
					__( 'Batch %1$d (%2$d orders) completed in %3$d seconds', 'woocommerce' ),
					$batch_count,
					count( $order_ids ),
					$batch_total_time
				)
			);

			$batch_count ++;
			$total_time += $batch_total_time;

			$progress->tick();

			$remaining_count = $this->count_unmigrated( array(), array( 'log' => false ) );
			if ( $remaining_count === $order_count ) {
				return WP_CLI::error( __( 'Infinite loop detected, aborting.', 'woocommerce' ) );
			}
			$order_count = $remaining_count;
		}

		$progress->finish();

		// Issue a warning if no orders were migrated.
		if ( ! $processed ) {
			return WP_CLI::warning( __( 'No orders were synced.', 'woocommerce' ) );
		}

		WP_CLI::log( __( 'Sync completed.', 'woocommerce' ) );

		return WP_CLI::success(
			sprintf(
				/* Translators: %1$d is the number of migrated orders and %2$d is the execution time in seconds. */
				_n(
					'%1$d order was synced in %2$d seconds.',
					'%1$d orders were synced in %2$d seconds.',
					$processed,
					'woocommerce'
				),
				$processed,
				$total_time
			)
		);
	}

	/**
	 * Copy order data into the postmeta table.
	 *
	 * Note that this could dramatically increase the size of your postmeta table, but is recommended
	 * if you wish to stop using the custom orders table plugin.
	 *
	 * ## OPTIONS
	 *
	 * [--batch-size=<batch-size>]
	 * : The number of orders to process in each batch. Passing a value of 0 will disable batching.
	 * ---
	 * default: 500
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     # Copy all order data into the post meta table, 500 posts at a time.
	 *     wp wc cot backfill --batch-size=500
	 *
	 * @param array $args Positional arguments passed to the command.
	 * @param array $assoc_args Associative arguments (options) passed to the command.
	 */
	public function migrate( $args = array(), $assoc_args = array() ) {
		$this->log_production_warning();
		WP_CLI::log( __( 'Migrate command is deprecated. Please use `sync` instead.', 'woocommerce' ) );
	}

	/**
	 * Verify migrated order data with original posts data.
	 *
	 * ## OPTIONS
	 *
	 * [--batch-size=<batch-size>]
	 * : The number of orders to verify in each batch.
	 * ---
	 * default: 500
	 * ---
	 *
	 * [--start-from=<order_id>]
	 * : Order ID to start from.
	 * ---
	 * default: 0
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     # Verify migrated order data, 500 orders at a time.
	 *     wp wc cot verify_cot_data --batch-size=500 --start-from=0
	 *
	 * @param array $args Positional arguments passed to the command.
	 * @param array $assoc_args Associative arguments (options) passed to the command.
	 */
	public function verify_cot_data( $args = array(), $assoc_args = array() ) {
		global $wpdb;
		$this->log_production_warning();
		if ( ! $this->is_enabled() ) {
			return;
		}

		$assoc_args = wp_parse_args(
			$assoc_args,
			array(
				'batch-size' => 500,
				'start-from' => 0,
			)
		);

		$batch_count    = 1;
		$total_time     = 0;
		$failed_ids     = array();
		$processed      = 0;
		$order_id_start = (int) $assoc_args['start-from'];
		$order_count    = $this->get_verify_order_count( $order_id_start );
		$batch_size     = ( (int) $assoc_args['batch-size'] ) === 0 ? 500 : (int) $assoc_args['batch-size'];

		$progress = WP_CLI\Utils\make_progress_bar( 'Order Data Verification', $order_count / $batch_size );

		if ( ! $order_count ) {
			return WP_CLI::warning( __( 'There are no orders to verify, aborting.', 'woocommerce' ) );
		}

		while ( $order_count > 0 ) {
			WP_CLI::debug(
				sprintf(
					/* Translators: %1$d is the batch number, %2$d is the batch size. */
					__( 'Beginning verification for batch #%1$d (%2$d orders/batch).', 'woocommerce' ),
					$batch_count,
					$batch_size
				)
			);

			$order_ids        = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order' AND ID > %d ORDER BY ID ASC LIMIT %d",
					$order_id_start,
					$batch_size
				)
			);
			$batch_start_time = microtime( true );
			$failed_ids       = $failed_ids + $this->post_to_cot_migrator->verify_migrated_orders( $order_ids );
			$failed_ids       = $this->verify_meta_data( $order_ids, $failed_ids );
			$processed       += count( $order_ids );
			$batch_total_time = microtime( true ) - $batch_start_time;
			$batch_count ++;
			$total_time += $batch_total_time;

			$progress->tick();

			WP_CLI::debug(
				sprintf(
					/* Translators: %1$d is the batch number, %2$d is time taken to process batch. */
					__( 'Batch %1$d (%2$d orders) completed in %3$d seconds.', 'woocommerce' ),
					$batch_count,
					count( $order_ids ),
					$batch_total_time
				)
			);

			$order_id_start  = max( $order_ids );
			$remaining_count = $this->get_verify_order_count( $order_id_start, false );
			if ( $remaining_count === $order_count ) {
				return WP_CLI::error( __( 'Infinite loop detected, aborting. No errors found.', 'woocommerce' ) );
			}
			$order_count = $remaining_count;
		}

		$progress->finish();
		WP_CLI::log( __( 'Verification completed.', 'woocommerce' ) );

		if ( 0 === count( $failed_ids ) ) {
			return WP_CLI::success(
				sprintf(
					/* Translators: %1$d is the number of migrated orders and %2$d is time taken. */
					_n(
						'%1$d order was verified in %2$d seconds.',
						'%1$d orders were verified in %2$d seconds.',
						$processed,
						'woocommerce'
					),
					$processed,
					$total_time
				)
			);
		} else {
			$errors = print_r( $failed_ids, true );

			return WP_CLI::error(
				sprintf(
					'%1$s %2$s',
					sprintf(
						/* Translators: %1$d is the number of migrated orders and %2$d is the execution time in seconds. */
						_n(
							'%1$d order was verified in %2$d seconds.',
							'%1$d orders were verified in %2$d seconds.',
							$processed,
							'woocommerce'
						),
						$processed,
						$total_time
					),
					sprintf(
						/* Translators: %1$d is number of errors and %2$s is the formatted array of order IDs. */
						_n(
							'%1$d error found: %2$s. Please review the error above.',
							'%1$d errors found: %2$s. Please review the errors above.',
							count( $failed_ids ),
							'woocommerce'
						),
						count( $failed_ids ),
						$errors
					)
				)
			);
		}
	}

	/**
	 * Helper method to get count for orders needing verification.
	 *
	 * @param int  $order_id_start Order ID to start from.
	 * @param bool $log Whether to also log an error message.
	 *
	 * @return int Order count.
	 */
	private function get_verify_order_count( int $order_id_start, $log = true ) : int {
		global $wpdb;

		$order_count = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = 'shop_order' AND ID > %d",
				$order_id_start
			)
		);

		if ( $log ) {
			WP_CLI::log(
				sprintf(
					/* Translators: %1$d is the number of orders to be verified. */
					_n(
						'There is %1$d order to be verified.',
						'There are %1$d orders to be verified.',
						$order_count,
						'woocommerce'
					),
					$order_count
				)
			);
		}

		return $order_count;
	}

	/**
	 * Verify meta data as part of verifying the order object.
	 *
	 * @param array $order_ids Order IDs.
	 * @param array $failed_ids Array for storing failed IDs.
	 *
	 * @return array Failed IDs with meta details.
	 */
	private function verify_meta_data( array $order_ids, array $failed_ids ) : array {
		global $wpdb;
		if ( ! count( $order_ids ) ) {
			return array();
		}
		$excluded_columns             = $this->post_to_cot_migrator->get_migrated_meta_keys();
		$excluded_columns_placeholder = implode( ', ', array_fill( 0, count( $excluded_columns ), '%s' ) );
		$order_ids_placeholder        = implode( ', ', array_fill( 0, count( $order_ids ), '%d' ) );
		$meta_table                   = OrdersTableDataStore::get_meta_table_name();

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- table names are hardcoded, orders_ids and excluded_columns are prepared.
		$query           = $wpdb->prepare(
			"
SELECT {$wpdb->postmeta}.post_id as entity_id, {$wpdb->postmeta}.meta_key, {$wpdb->postmeta}.meta_value
FROM $wpdb->postmeta
WHERE
      {$wpdb->postmeta}.post_id in ( $order_ids_placeholder ) AND
      {$wpdb->postmeta}.meta_key not in ( $excluded_columns_placeholder )
ORDER BY {$wpdb->postmeta}.post_id ASC, {$wpdb->postmeta}.meta_key ASC;
",
			array_merge(
				$order_ids,
				$excluded_columns
			)
		);
		$source_data = $wpdb->get_results( $query, ARRAY_A );
		// phpcs:enable

		$normalized_source_data = $this->normalize_raw_meta_data( $source_data );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- table names are hardcoded, orders_ids and excluded_columns are prepared.
		$migrated_query = $wpdb->prepare(
			"
SELECT $meta_table.order_id as entity_id, $meta_table.meta_key, $meta_table.meta_value
FROM $meta_table
WHERE
	$meta_table.order_id in ( $order_ids_placeholder )
ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
",
			$order_ids
		);
		$migrated_data  = $wpdb->get_results( $migrated_query, ARRAY_A );
		// phpcs:enable

		$normalized_migrated_meta_data = $this->normalize_raw_meta_data( $migrated_data );

		foreach ( $normalized_source_data as $order_id => $meta ) {
			foreach ( $meta as $meta_key => $values ) {
				$migrated_meta_values = isset( $normalized_migrated_meta_data[ $order_id ][ $meta_key ] ) ? $normalized_migrated_meta_data[ $order_id ][ $meta_key ] : array();
				$diff = array_diff( $values, $migrated_meta_values );

				if ( count( $diff ) ) {
					if ( ! isset( $failed_ids[ $order_id ] ) ) {
						$failed_ids[ $order_id ] = array();
					}
					$failed_ids[ $order_id ][] = array(
						'order_id'   => $order_id,
						'meta_key'   => $meta_key,
						'orig_meta_values' => $values,
						'new_meta_values' => $migrated_meta_values,
					);
				}
			}
		}

		return $failed_ids;
	}

	/**
	 * Helper method to normalize response from meta queries into order_id > meta_key > meta_values.
	 *
	 * @param array $data Data fetched from meta queries.
	 *
	 * @return array Normalized data.
	 */
	private function normalize_raw_meta_data( array $data ) : array {
		$clubbed_data = array();
		foreach ( $data as $row ) {
			if ( ! isset( $clubbed_data[ $row['entity_id'] ] ) ) {
				$clubbed_data[ $row['entity_id'] ] = array();
			}
			if ( ! isset( $clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ] ) ) {
				$clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ] = array();
			}
			$clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ][] = $row['meta_value'];
		}
		return $clubbed_data;
	}

}
Database/Migrations/CustomOrderTable/PostToOrderOpTableMigrator.php000064400000007623152333011040021513 0ustar00<?php
/**
 * Class for WPPost to wc_order_operational_details migrator.
 */

namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;

use Automattic\WooCommerce\Database\Migrations\MetaToCustomTableMigrator;

/**
 * Helper class to migrate records from the WordPress post table
 * to the custom order operations table.
 *
 * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
 */
class PostToOrderOpTableMigrator extends MetaToCustomTableMigrator {

	/**
	 * Get schema config for wp_posts and wc_order_operational_detail table.
	 *
	 * @return array Config.
	 */
	protected function get_schema_config(): array {
		global $wpdb;
		// TODO: Remove hardcoding.
		$this->table_names = array(
			'orders'    => $wpdb->prefix . 'wc_orders',
			'addresses' => $wpdb->prefix . 'wc_order_addresses',
			'op_data'   => $wpdb->prefix . 'wc_order_operational_data',
			'meta'      => $wpdb->prefix . 'wc_orders_meta',
		);

		return array(
			'source'      => array(
				'entity' => array(
					'table_name'             => $this->table_names['orders'],
					'meta_rel_column'        => 'id',
					'destination_rel_column' => 'id',
					'primary_key'            => 'id',
				),
				'meta'   => array(
					'table_name'        => $wpdb->postmeta,
					'meta_key_column'   => 'meta_key',
					'meta_value_column' => 'meta_value',
					'entity_id_column'  => 'post_id',
				),
			),
			'destination' => array(
				'table_name'        => $this->table_names['op_data'],
				'source_rel_column' => 'order_id',
				'primary_key'       => 'id',
				'primary_key_type'  => 'int',
			),
		);
	}


	/**
	 * Get columns config.
	 *
	 * @return \string[][] Config.
	 */
	protected function get_core_column_mapping(): array {
		return array(
			'id' => array(
				'type'        => 'int',
				'destination' => 'order_id',
			),
		);
	}


	/**
	 * Get meta data config.
	 *
	 * @return \string[][] Config.
	 */
	public function get_meta_column_config(): array {
		return array(
			'_created_via'                  => array(
				'type'        => 'string',
				'destination' => 'created_via',
			),
			'_order_version'                => array(
				'type'        => 'string',
				'destination' => 'woocommerce_version',
			),
			'_prices_include_tax'           => array(
				'type'        => 'bool',
				'destination' => 'prices_include_tax',
			),
			'_recorded_coupon_usage_counts' => array(
				'type'        => 'bool',
				'destination' => 'coupon_usages_are_counted',
			),
			'_download_permissions_granted' => array(
				'type'        => 'bool',
				'destination' => 'download_permission_granted',
			),
			'_cart_hash'                    => array(
				'type'        => 'string',
				'destination' => 'cart_hash',
			),
			'_new_order_email_sent'         => array(
				'type'        => 'bool',
				'destination' => 'new_order_email_sent',
			),
			'_order_key'                    => array(
				'type'        => 'string',
				'destination' => 'order_key',
			),
			'_order_stock_reduced'          => array(
				'type'        => 'bool',
				'destination' => 'order_stock_reduced',
			),
			'_date_paid'                    => array(
				'type'        => 'date_epoch',
				'destination' => 'date_paid_gmt',
			),
			'_date_completed'               => array(
				'type'        => 'date_epoch',
				'destination' => 'date_completed_gmt',
			),
			'_order_shipping_tax'           => array(
				'type'        => 'decimal',
				'destination' => 'shipping_tax_amount',
			),
			'_order_shipping'               => array(
				'type'        => 'decimal',
				'destination' => 'shipping_total_amount',
			),
			'_cart_discount_tax'            => array(
				'type'        => 'decimal',
				'destination' => 'discount_tax_amount',
			),
			'_cart_discount'                => array(
				'type'        => 'decimal',
				'destination' => 'discount_total_amount',
			),
			'_recorded_sales'               => array(
				'type'        => 'bool',
				'destination' => 'recorded_sales',
			),
		);
	}
}
Database/Migrations/CustomOrderTable/PostMetaToOrderMetaMigrator.php000064400000004053152333011040021654 0ustar00<?php
/**
 * Migration class for migrating from WPPostMeta to OrderMeta table.
 */

namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;

use Automattic\WooCommerce\Database\Migrations\MetaToMetaTableMigrator;

/**
 * Helper class to migrate records from the WordPress post meta table
 * to the custom orders meta table.
 *
 * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
 */
class PostMetaToOrderMetaMigrator extends MetaToMetaTableMigrator {

	/**
	 * List of meta keys to exclude from migration.
	 *
	 * @var array
	 */
	private $excluded_columns;

	/**
	 * PostMetaToOrderMetaMigrator constructor.
	 *
	 * @param array $excluded_columns List of meta keys to exclude from migration.
	 */
	public function __construct( $excluded_columns ) {
		$this->excluded_columns = $excluded_columns;
		parent::__construct();
	}

	/**
	 * Generate config for meta data migration.
	 *
	 * @return array Meta data migration config.
	 */
	protected function get_meta_config(): array {
		global $wpdb;
		// TODO: Remove hardcoding.
		$this->table_names = array(
			'orders'    => $wpdb->prefix . 'wc_orders',
			'addresses' => $wpdb->prefix . 'wc_order_addresses',
			'op_data'   => $wpdb->prefix . 'wc_order_operational_data',
			'meta'      => $wpdb->prefix . 'wc_orders_meta',
		);

		return array(
			'source'      => array(
				'meta'          => array(
					'table_name'        => $wpdb->postmeta,
					'entity_id_column'  => 'post_id',
					'meta_key_column'   => 'meta_key',
					'meta_value_column' => 'meta_value',
				),
				'entity'        => array(
					'table_name'       => $this->table_names['orders'],
					'source_id_column' => 'id',
					'id_column'        => 'id',
				),
				'excluded_keys' => $this->excluded_columns,
			),
			'destination' => array(
				'meta' => array(
					'table_name'        => $this->table_names['meta'],
					'entity_id_column'  => 'order_id',
					'meta_key_column'   => 'meta_key',
					'meta_value_column' => 'meta_value',
					'entity_id_type'    => 'int',
					'meta_id_column'    => 'id',
				),
			),
		);
	}
}
Database/Migrations/CustomOrderTable/PostsToOrdersMigrationController.php000064400000017575152333011040023032 0ustar00<?php
/**
 * Class for implementing migration from wp_posts and wp_postmeta to custom order tables.
 */

namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;

use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Utilities\ArrayUtil;

/**
 * This is the main class used to perform the complete migration of orders
 * from the posts table to the custom orders table.
 *
 * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
 */
class PostsToOrdersMigrationController {

	/**
	 * Error logger for migration errors.
	 *
	 * @var \WC_Logger
	 */
	private $error_logger;

	/**
	 * Array of objects used to perform the migration.
	 *
	 * @var array
	 */
	private $all_migrators;

	/**
	 * The source name to use for logs.
	 */
	public const LOGS_SOURCE_NAME = 'posts-to-orders-migration';

	/**
	 * PostsToOrdersMigrationController constructor.
	 */
	public function __construct() {

		$this->all_migrators   = array();
		$this->all_migrators[] = new PostToOrderTableMigrator();
		$this->all_migrators[] = new PostToOrderAddressTableMigrator( 'billing' );
		$this->all_migrators[] = new PostToOrderAddressTableMigrator( 'shipping' );
		$this->all_migrators[] = new PostToOrderOpTableMigrator();
		$this->all_migrators[] = new PostMetaToOrderMetaMigrator( $this->get_migrated_meta_keys() );
		$this->error_logger    = wc_get_logger();
	}

	/**
	 * Helper method to get migrated keys for all the tables in this controller.
	 *
	 * @return string[] Array of meta keys.
	 */
	public function get_migrated_meta_keys() {
		$migrated_meta_keys = array();
		foreach ( $this->all_migrators as $migrator ) {
			if ( method_exists( $migrator, 'get_meta_column_config' ) ) {
				$migrated_meta_keys = array_merge( $migrated_meta_keys, $migrator->get_meta_column_config() );
			}
		}
		return array_keys( $migrated_meta_keys );
	}

	/**
	 * Migrates a set of orders from the posts table to the custom orders tables.
	 *
	 * @param array $order_post_ids List of post IDs of the orders to migrate.
	 */
	public function migrate_orders( array $order_post_ids ): void {
		$this->error_logger = WC()->call_function( 'wc_get_logger' );

		$using_transactions = $this->maybe_start_transaction();
		if ( null === $using_transactions ) {
			return;
		}

		$errors_were_logged = false;

		foreach ( $this->all_migrators as $migrator ) {
			$errors_were_logged = $this->do_orders_migration_step( $migrator, $order_post_ids );
			if ( $errors_were_logged && $using_transactions ) {
				$this->rollback_transaction();
				break;
			}
		}

		if ( ! $errors_were_logged && $using_transactions ) {
			$this->commit_transaction();
		}
	}

	/**
	 * Start a database transaction if the configuration mandates so.
	 *
	 * @return bool|null True if transaction started, false if transactions won't be used, null if transaction failed to start.
	 */
	private function maybe_start_transaction(): ?bool {
		if ( 'yes' !== get_option( CustomOrdersTableController::USE_DB_TRANSACTIONS_OPTION ) ) {
			return false;
		}

		$transaction_isolation_level = get_option( CustomOrdersTableController::DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION, CustomOrdersTableController::DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL );
		$this->verify_transaction_isolation_level( $transaction_isolation_level );
		$set_transaction_isolation_level_command = "SET TRANSACTION ISOLATION LEVEL $transaction_isolation_level";

		if ( ! $this->db_query( $set_transaction_isolation_level_command ) ) {
			return null;
		}

		return $this->db_query( 'START TRANSACTION' ) ? true : null;
	}

	/**
	 * Verify that a given database transaction isolation level name is valid, and throw an exception if not.
	 *
	 * @param string $transaction_isolation_level Transaction isolation level name to check.
	 * @return void
	 * @throws \Exception Invalid transaction isolation level name.
	 */
	private function verify_transaction_isolation_level( string $transaction_isolation_level ): void {
		if ( ! in_array( $transaction_isolation_level, CustomOrdersTableController::get_valid_transaction_isolation_levels(), true ) ) {
			throw new \Exception( 'Invalid database transaction isolation level name ' . $transaction_isolation_level );
		}
	}

	/**
	 * Commit the current database transaction.
	 *
	 * @return bool True on success, false on error.
	 */
	private function commit_transaction(): bool {
		return $this->db_query( 'COMMIT' );
	}

	/**
	 * Rollback the current database transaction.
	 *
	 * @return bool True on success, false on error.
	 */
	private function rollback_transaction(): bool {
		return $this->db_query( 'ROLLBACK' );
	}

	/**
	 * Execute a database query and log any errors.
	 *
	 * @param string $query The SQL query to execute.
	 * @return bool True if the query succeeded, false if there were errors.
	 */
	private function db_query( string $query ): bool {
		$wpdb = WC()->get_global( 'wpdb' );

		try {
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$wpdb->query( $query );
		} catch ( \Exception $exception ) {
			$exception_class = get_class( $exception );
			$this->error_logger->error(
				"PostsToOrdersMigrationController: when executing $query: ($exception_class) {$exception->getMessage()}, {$exception->getTraceAsString()}",
				array(
					'source'    => self::LOGS_SOURCE_NAME,
					'exception' => $exception,
				)
			);
			return false;
		}

		$error = $wpdb->last_error;
		if ( '' !== $error ) {
			$this->error_logger->error(
				"PostsToOrdersMigrationController: when executing $query: $error",
				array(
					'source' => self::LOGS_SOURCE_NAME,
					'error'  => $error,
				)
			);
			return false;
		}

		return true;
	}

	/**
	 * Performs one step of the migration for a set of order posts using one given migration class.
	 * All database errors and exceptions are logged.
	 *
	 * @param object $migration_class The migration class to use, must have a `process_migration_batch_for_ids(array of ids)` method.
	 * @param array  $order_post_ids List of post IDs of the orders to migrate.
	 * @return bool True if errors were logged, false otherwise.
	 */
	private function do_orders_migration_step( object $migration_class, array $order_post_ids ): bool {
		$result = $migration_class->process_migration_batch_for_ids( $order_post_ids );

		$errors    = array_unique( $result['errors'] );
		$exception = $result['exception'];
		if ( null === $exception && empty( $errors ) ) {
			return false;
		}

		$migration_class_name = ( new \ReflectionClass( $migration_class ) )->getShortName();
		$batch                = ArrayUtil::to_ranges_string( $order_post_ids );

		if ( null !== $exception ) {
			$exception_class = get_class( $exception );
			$this->error_logger->error(
				"$migration_class_name: when processing ids $batch: ($exception_class) {$exception->getMessage()}, {$exception->getTraceAsString()}",
				array(
					'source'    => self::LOGS_SOURCE_NAME,
					'ids'       => $order_post_ids,
					'exception' => $exception,
				)
			);
		}

		foreach ( $errors as $error ) {
			$this->error_logger->error(
				"$migration_class_name: when processing ids $batch: $error",
				array(
					'source' => self::LOGS_SOURCE_NAME,
					'ids'    => $order_post_ids,
					'error'  => $error,
				)
			);
		}

		return true;
	}

	/**
	 * Verify whether the given order IDs were migrated properly or not.
	 *
	 * @param array $order_post_ids Order IDs.
	 *
	 * @return array Array of failed IDs along with columns.
	 */
	public function verify_migrated_orders( array $order_post_ids ): array {
		$errors = array();
		foreach ( $this->all_migrators as $migrator ) {
			if ( method_exists( $migrator, 'verify_migrated_data' ) ) {
				$errors = $errors + $migrator->verify_migrated_data( $order_post_ids );
			}
		}
		return $errors;
	}

	/**
	 * Migrates an order from the posts table to the custom orders tables.
	 *
	 * @param int $order_post_id Post ID of the order to migrate.
	 */
	public function migrate_order( int $order_post_id ): void {
		$this->migrate_orders( array( $order_post_id ) );
	}
}
Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php000064400000011111152333011040022505 0ustar00<?php
/**
 * Class for WPPost to wc_order_address table migrator.
 */

namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;

use Automattic\WooCommerce\Database\Migrations\MetaToCustomTableMigrator;

/**
 * Helper class to migrate records from the WordPress post table
 * to the custom order addresses table.
 *
 * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
 */
class PostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
	/**
	 * Type of addresses being migrated, could be billing|shipping.
	 *
	 * @var $type
	 */
	protected $type;

	/**
	 * PostToOrderAddressTableMigrator constructor.
	 *
	 * @param string $type Type of addresses being migrated, could be billing|shipping.
	 */
	public function __construct( $type ) {
		$this->type = $type;
		parent::__construct();
	}

	/**
	 * Get schema config for wp_posts and wc_order_address table.
	 *
	 * @return array Config.
	 */
	protected function get_schema_config(): array {
		global $wpdb;
		// TODO: Remove hardcoding.
		$this->table_names = array(
			'orders'    => $wpdb->prefix . 'wc_orders',
			'addresses' => $wpdb->prefix . 'wc_order_addresses',
			'op_data'   => $wpdb->prefix . 'wc_order_operational_data',
			'meta'      => $wpdb->prefix . 'wc_orders_meta',
		);

		return array(
			'source'      => array(
				'entity' => array(
					'table_name'             => $this->table_names['orders'],
					'meta_rel_column'        => 'id',
					'destination_rel_column' => 'id',
					'primary_key'            => 'id',
				),
				'meta'   => array(
					'table_name'        => $wpdb->postmeta,
					'meta_key_column'   => 'meta_key',
					'meta_value_column' => 'meta_value',
					'entity_id_column'  => 'post_id',
				),
			),
			'destination' => array(
				'table_name'        => $this->table_names['addresses'],
				'source_rel_column' => 'order_id',
				'primary_key'       => 'id',
				'primary_key_type'  => 'int',
			),
		);
	}

	/**
	 * Get columns config.
	 *
	 * @return \string[][] Config.
	 */
	protected function get_core_column_mapping(): array {
		$type = $this->type;

		return array(
			'id'   => array(
				'type'        => 'int',
				'destination' => 'order_id',
			),
			'type' => array(
				'type'          => 'string',
				'destination'   => 'address_type',
				'select_clause' => "'$type'",
			),
		);
	}

	/**
	 * Get meta data config.
	 *
	 * @return \string[][] Config.
	 */
	public function get_meta_column_config(): array {
		$type = $this->type;

		return array(
			"_{$type}_first_name" => array(
				'type'        => 'string',
				'destination' => 'first_name',
			),
			"_{$type}_last_name"  => array(
				'type'        => 'string',
				'destination' => 'last_name',
			),
			"_{$type}_company"    => array(
				'type'        => 'string',
				'destination' => 'company',
			),
			"_{$type}_address_1"  => array(
				'type'        => 'string',
				'destination' => 'address_1',
			),
			"_{$type}_address_2"  => array(
				'type'        => 'string',
				'destination' => 'address_2',
			),
			"_{$type}_city"       => array(
				'type'        => 'string',
				'destination' => 'city',
			),
			"_{$type}_state"      => array(
				'type'        => 'string',
				'destination' => 'state',
			),
			"_{$type}_postcode"   => array(
				'type'        => 'string',
				'destination' => 'postcode',
			),
			"_{$type}_country"    => array(
				'type'        => 'string',
				'destination' => 'country',
			),
			"_{$type}_email"      => array(
				'type'        => 'string',
				'destination' => 'email',
			),
			"_{$type}_phone"      => array(
				'type'        => 'string',
				'destination' => 'phone',
			),
		);
	}

	/**
	 * Additional WHERE clause to only fetch the addresses of the current type.
	 *
	 * @param array $entity_ids The ids of the entities being inserted or updated.
	 * @return string The additional string for the WHERE clause.
	 */
	protected function get_additional_where_clause_for_get_data_to_insert_or_update( array $entity_ids ): string {
		return "AND destination.`address_type` = '{$this->type}'";
	}

	/**
	 * Helper function to generate where clause for fetching data for verification.
	 *
	 * @param array $source_ids Array of IDs from source table.
	 *
	 * @return string WHERE clause.
	 */
	protected function get_where_clause_for_verification( $source_ids ) {
		global $wpdb;
		$query = parent::get_where_clause_for_verification( $source_ids );
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $query should already be prepared, $schema_config is hardcoded.
		return $wpdb->prepare( "$query AND {$this->schema_config['destination']['table_name']}.address_type = %s", $this->type );
	}
}
Database/Migrations/TableMigrator.php000064400000007131152333011040013623 0ustar00<?php
/**
 * Base class for all the WP posts to order table migrator.
 */

namespace Automattic\WooCommerce\Database\Migrations;

/**
 * Base class for implementing WP posts to order tables migrations handlers.
 * It mainly contains methods to deal with error handling.
 *
 * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
 */
abstract class TableMigrator {

	/**
	 * An array of cummulated error messages.
	 *
	 * @var array
	 */
	private $errors;

	/**
	 * Clear the error messages list.
	 *
	 * @return void
	 */
	protected function clear_errors(): void {
		$this->errors = array();
	}

	/**
	 * Add an error message to the errors list unless it's there already.
	 *
	 * @param string $error The error message to add.
	 * @return void
	 */
	protected function add_error( string $error ): void {
		if ( ! in_array( $error, $this->errors, true ) ) {
			$this->errors[] = $error;
		}
	}

	/**
	 * Get the list of error messages added.
	 *
	 * @return array
	 */
	protected function get_errors(): array {
		return $this->errors;
	}

	/**
	 * Run $wpdb->query and add the error, if any, to the errors list.
	 *
	 * @param string $query The SQL query to run.
	 * @return mixed Whatever $wpdb->query returns.
	 */
	protected function db_query( string $query ) {
		$wpdb = WC()->get_global( 'wpdb' );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$result = $wpdb->query( $query );

		if ( '' !== $wpdb->last_error ) {
			$this->add_error( $wpdb->last_error );
		}

		return $result;
	}

	/**
	 * Run $wpdb->get_results and add the error, if any, to the errors list.
	 *
	 * @param string|null $query The SQL query to run.
	 * @param string      $output Any of ARRAY_A | ARRAY_N | OBJECT | OBJECT_K constants.
	 * @return mixed Whatever $wpdb->get_results returns.
	 */
	protected function db_get_results( string $query = null, string $output = OBJECT ) {
		$wpdb = WC()->get_global( 'wpdb' );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$result = $wpdb->get_results( $query, $output );

		if ( '' !== $wpdb->last_error ) {
			$this->add_error( $wpdb->last_error );
		}

		return $result;
	}

	/**
	 * Migrate a batch of orders, logging any database error that could arise and the exception thrown if any.
	 *
	 * @param array $entity_ids Order ids to migrate.
	 * @return array An array containing the keys 'errors' (array of strings) and 'exception' (exception object or null).
	 */
	public function process_migration_batch_for_ids( array $entity_ids ): array {
		$this->clear_errors();
		$exception = null;

		try {
			$this->process_migration_batch_for_ids_core( $entity_ids );
		} catch ( \Exception $ex ) {
			$exception = $ex;
		}

		return array(
			'errors'    => $this->get_errors(),
			'exception' => $exception,
		);
	}

	/**
	 * The core method that actually performs the migration for the supplied batch of order ids.
	 * It doesn't need to deal with database errors nor with exceptions.
	 *
	 * @param array $entity_ids Order ids to migrate.
	 * @return void
	 */
	abstract protected function process_migration_batch_for_ids_core( array $entity_ids ): void;

	/**
	 * Check if the amount of processed database rows matches the amount of orders to process, and log an error if not.
	 *
	 * @param string     $operation Operation performed, 'insert' or 'update'.
	 * @param array|bool $received_rows_count Value returned by @wpdb after executing the query.
	 * @return void
	 */
	protected function maybe_add_insert_or_update_error( string $operation, $received_rows_count ) {
		if ( false === $received_rows_count ) {
			$this->add_error( "$operation operation didn't complete, the database query failed" );
		}
	}
}
Database/Migrations/MigrationHelper.php000064400000026102152333011040014157 0ustar00<?php
/**
 * Helper class with utility functions for migrations.
 */

namespace Automattic\WooCommerce\Database\Migrations;

use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\StringUtil;

/**
 * Helper class to assist with migration related operations.
 */
class MigrationHelper {

	/**
	 * Placeholders that we will use in building $wpdb queries.
	 *
	 * @var string[]
	 */
	private static $wpdb_placeholder_for_type = array(
		'int'        => '%d',
		'decimal'    => '%f',
		'string'     => '%s',
		'date'       => '%s',
		'date_epoch' => '%s',
		'bool'       => '%d',
	);

	/**
	 * Helper method to escape backtick in various schema fields.
	 *
	 * @param array $schema_config Schema config.
	 *
	 * @return array Schema config escaped for backtick.
	 */
	public static function escape_schema_for_backtick( array $schema_config ): array {
		array_walk( $schema_config['source']['entity'], array( self::class, 'escape_and_add_backtick' ) );
		array_walk( $schema_config['source']['meta'], array( self::class, 'escape_and_add_backtick' ) );
		array_walk( $schema_config['destination'], array( self::class, 'escape_and_add_backtick' ) );
		return $schema_config;
	}

	/**
	 * Helper method to escape backtick in column and table names.
	 * WP does not provide a method to escape table/columns names yet, but hopefully soon in @link https://core.trac.wordpress.org/ticket/52506
	 *
	 * @param string|array $identifier Column or table name.
	 *
	 * @return array|string|string[] Escaped identifier.
	 */
	public static function escape_and_add_backtick( $identifier ) {
		return '`' . str_replace( '`', '``', $identifier ) . '`';
	}

	/**
	 * Return $wpdb->prepare placeholder for data type.
	 *
	 * @param string $type Data type.
	 *
	 * @return string $wpdb placeholder.
	 */
	public static function get_wpdb_placeholder_for_type( string $type ): string {
		return self::$wpdb_placeholder_for_type[ $type ];
	}

	/**
	 * Generates ON DUPLICATE KEY UPDATE clause to be used in migration.
	 *
	 * @param array $columns List of column names.
	 *
	 * @return string SQL clause for INSERT...ON DUPLICATE KEY UPDATE
	 */
	public static function generate_on_duplicate_statement_clause( array $columns ): string {
		$db_util = wc_get_container()->get( DatabaseUtil::class );
		return $db_util->generate_on_duplicate_statement_clause( $columns );
	}

	/**
	 * Migrate state codes in all the required places in the database, needed after they change for a given country.
	 *
	 * @param string $country_code The country that has the states for which the migration is needed.
	 * @param array  $old_to_new_states_mapping An associative array where keys are the old state codes and values are the new state codes.
	 * @return bool True if there are more records that need to be migrated, false otherwise.
	 */
	public static function migrate_country_states( string $country_code, array $old_to_new_states_mapping ): bool {
		$more_remaining = self::migrate_country_states_for_orders( $country_code, $old_to_new_states_mapping );
		if ( ! $more_remaining ) {
			self::migrate_country_states_for_misc_data( $country_code, $old_to_new_states_mapping );
		}
		return $more_remaining;
	}

	/**
	 * Migrate state codes in all the required places in the database (except orders).
	 *
	 * @param string $country_code The country that has the states for which the migration is needed.
	 * @param array  $old_to_new_states_mapping An associative array where keys are the old state codes and values are the new state codes.
	 * @return void
	 */
	private static function migrate_country_states_for_misc_data( string $country_code, array $old_to_new_states_mapping ): void {
		self::migrate_country_states_for_shipping_locations( $country_code, $old_to_new_states_mapping );
		self::migrate_country_states_for_tax_rates( $country_code, $old_to_new_states_mapping );
		self::migrate_country_states_for_store_location( $country_code, $old_to_new_states_mapping );
	}

	/**
	 * Migrate state codes in the shipping locations table.
	 *
	 * @param string $country_code The country that has the states for which the migration is needed.
	 * @param array  $old_to_new_states_mapping An associative array where keys are the old state codes and values are the new state codes.
	 * @return void
	 */
	private static function migrate_country_states_for_shipping_locations( string $country_code, array $old_to_new_states_mapping ): void {
		global $wpdb;

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared

		$sql            = "SELECT location_id, location_code FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE location_code LIKE '{$country_code}:%'";
		$locations_data = $wpdb->get_results( $sql, ARRAY_A );

		foreach ( $locations_data as $location_data ) {
			$old_state_code = substr( $location_data['location_code'], 3 );
			if ( array_key_exists( $old_state_code, $old_to_new_states_mapping ) ) {
				$new_location_code = "{$country_code}:{$old_to_new_states_mapping[$old_state_code]}";
				$update_query      = $wpdb->prepare(
					"UPDATE {$wpdb->prefix}woocommerce_shipping_zone_locations SET location_code=%s WHERE location_id=%d",
					$new_location_code,
					$location_data['location_id']
				);
				$wpdb->query( $update_query );
			}
		}

		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Migrate the state code for the store location.
	 *
	 * @param string $country_code The country that has the states for which the migration is needed.
	 * @param array  $old_to_new_states_mapping An associative array where keys are the old state codes and values are the new state codes.
	 * @return void
	 */
	private static function migrate_country_states_for_store_location( string $country_code, array $old_to_new_states_mapping ): void {
		$store_location = get_option( 'woocommerce_default_country', '' );
		if ( StringUtil::starts_with( $store_location, "{$country_code}:" ) ) {
			$old_location_code = substr( $store_location, 3 );
			if ( array_key_exists( $old_location_code, $old_to_new_states_mapping ) ) {
				$new_location_code = "{$country_code}:{$old_to_new_states_mapping[$old_location_code]}";
				update_option( 'woocommerce_default_country', $new_location_code );
			}
		}
	}

	/**
	 * Migrate state codes for orders in the orders table and in the posts table.
	 * It will migrate only N*2*(number of states) records, being N equal to 100 by default
	 * but this number can be modified via the woocommerce_migrate_country_states_for_orders_batch_size filter.
	 *
	 * @param string $country_code The country that has the states for which the migration is needed.
	 * @param array  $old_to_new_states_mapping An associative array where keys are the old state codes and values are the new state codes.
	 * @return bool True if there are more records that need to be migrated, false otherwise.
	 */
	private static function migrate_country_states_for_orders( string $country_code, array $old_to_new_states_mapping ): bool {
		global $wpdb;

		/**
		 * Filters the value of N, where the maximum count of database records that will be updated in one single run of migrate_country_states_for_orders
		 * is N*2*count($old_to_new_states_mapping) if the woocommerce_orders table exists, or N*count($old_to_new_states_mapping) otherwise.
		 *
		 * @param int $batch_size Default value for the count of records to update.
		 * @param string $country_code Country code for the update.
		 * @param array  $old_to_new_states_mapping Associative array of old to new state codes.
		 *
		 * @since 7.2.0
		 */
		$limit      = apply_filters( 'woocommerce_migrate_country_states_for_orders_batch_size', 100, $country_code, $old_to_new_states_mapping );
		$cot_exists = wc_get_container()->get( DataSynchronizer::class )->check_orders_table_exists();

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		foreach ( $old_to_new_states_mapping as $old_state => $new_state ) {
			if ( $cot_exists ) {
				$update_query = $wpdb->prepare(
					"UPDATE {$wpdb->prefix}wc_order_addresses SET state=%s WHERE country=%s AND state=%s LIMIT %d",
					$new_state,
					$country_code,
					$old_state,
					$limit
				);

				$wpdb->query( $update_query );
			}

			// We need to split the update query for the postmeta table in two, select + update,
			// because MySQL doesn't support the LIMIT keyword in multi-table UPDATE statements.

			$select_meta_ids_query = $wpdb->prepare(
				"SELECT meta_id FROM {$wpdb->prefix}postmeta,
					(SELECT DISTINCT post_id FROM {$wpdb->prefix}postmeta
					WHERE (meta_key = '_billing_country' OR meta_key='_shipping_country') AND meta_value=%s)
					AS states_in_country
				WHERE (meta_key='_billing_state' OR meta_key='_shipping_state')
				AND meta_value=%s
				AND {$wpdb->postmeta}.post_id = states_in_country.post_id
				LIMIT %d",
				$country_code,
				$old_state,
				$limit
			);

			$meta_ids = $wpdb->get_results( $select_meta_ids_query, ARRAY_A );
			if ( ! empty( $meta_ids ) ) {
				$meta_ids                    = ArrayUtil::select( $meta_ids, 'meta_id' );
				$meta_ids_as_comma_separated = '(' . join( ',', $meta_ids ) . ')';

				$update_query = $wpdb->prepare(
					"UPDATE {$wpdb->prefix}postmeta
					SET meta_value=%s
					WHERE meta_id IN {$meta_ids_as_comma_separated}",
					$new_state
				);

				$wpdb->query( $update_query );
			}
		}

		$states_as_comma_separated = "('" . join( "','", array_keys( $old_to_new_states_mapping ) ) . "')";

		$posts_exist_query = $wpdb->prepare(
			"
			SELECT 1 FROM {$wpdb->prefix}postmeta
			WHERE (meta_key='_billing_state' OR meta_key='_shipping_state')
			AND meta_value IN {$states_as_comma_separated}
			AND post_id IN (
				SELECT post_id FROM {$wpdb->prefix}postmeta WHERE
				(meta_key = '_billing_country' OR meta_key='_shipping_country')
				AND meta_value=%s
			)",
			$country_code
		);

		if ( $cot_exists ) {
			$more_exist_query = $wpdb->prepare(
				"
			SELECT EXISTS(
				SELECT 1 FROM {$wpdb->prefix}wc_order_addresses
				WHERE country=%s AND state IN {$states_as_comma_separated}
			)
			OR EXISTS (
			  {$posts_exist_query}
			)",
				$country_code
			);
		} else {
			$more_exist_query = "SELECT EXISTS ({$posts_exist_query})";
		}

		return (int) ( $wpdb->get_var( $more_exist_query ) ) !== 0;

		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Migrate state codes in the tax rates table.
	 *
	 * @param string $country_code The country that has the states for which the migration is needed.
	 * @param array  $old_to_new_states_mapping An associative array where keys are the old state codes and values are the new state codes.
	 * @return void
	 */
	private static function migrate_country_states_for_tax_rates( string $country_code, array $old_to_new_states_mapping ): void {
		global $wpdb;

		foreach ( $old_to_new_states_mapping as $old_state_code => $new_state_code ) {
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE {$wpdb->prefix}woocommerce_tax_rates SET tax_rate_state=%s WHERE tax_rate_country=%s AND tax_rate_state=%s",
					$new_state_code,
					$country_code,
					$old_state_code
				)
			);
		}
	}
}
Checkout/Helpers/ReserveStock.php000064400000015446152333011040013045 0ustar00<?php
/**
 * Handle product stock reservation during checkout.
 */

namespace Automattic\WooCommerce\Checkout\Helpers;

defined( 'ABSPATH' ) || exit;

/**
 * Stock Reservation class.
 */
final class ReserveStock {

	/**
	 * Is stock reservation enabled?
	 *
	 * @var boolean
	 */
	private $enabled = true;

	/**
	 * Constructor
	 */
	public function __construct() {
		// Table needed for this feature are added in 4.3.
		$this->enabled = get_option( 'woocommerce_schema_version', 0 ) >= 430;
	}

	/**
	 * Is stock reservation enabled?
	 *
	 * @return boolean
	 */
	protected function is_enabled() {
		return $this->enabled;
	}

	/**
	 * Query for any existing holds on stock for this item.
	 *
	 * @param \WC_Product $product Product to get reserved stock for.
	 * @param integer     $exclude_order_id Optional order to exclude from the results.
	 *
	 * @return integer Amount of stock already reserved.
	 */
	public function get_reserved_stock( $product, $exclude_order_id = 0 ) {
		global $wpdb;

		if ( ! $this->is_enabled() ) {
			return 0;
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
		return (int) $wpdb->get_var( $this->get_query_for_reserved_stock( $product->get_stock_managed_by_id(), $exclude_order_id ) );
	}

	/**
	 * Put a temporary hold on stock for an order if enough is available.
	 *
	 * @throws ReserveStockException If stock cannot be reserved.
	 *
	 * @param \WC_Order $order Order object.
	 * @param int       $minutes How long to reserve stock in minutes. Defaults to woocommerce_hold_stock_minutes.
	 */
	public function reserve_stock_for_order( $order, $minutes = 0 ) {
		$minutes = $minutes ? $minutes : (int) get_option( 'woocommerce_hold_stock_minutes', 60 );

		if ( ! $minutes || ! $this->is_enabled() ) {
			return;
		}

		try {
			$items = array_filter(
				$order->get_items(),
				function( $item ) {
					return $item->is_type( 'line_item' ) && $item->get_product() instanceof \WC_Product && $item->get_quantity() > 0;
				}
			);
			$rows  = array();

			foreach ( $items as $item ) {
				$product = $item->get_product();

				if ( ! $product->is_in_stock() ) {
					throw new ReserveStockException(
						'woocommerce_product_out_of_stock',
						sprintf(
							/* translators: %s: product name */
							__( '&quot;%s&quot; is out of stock and cannot be purchased.', 'woocommerce' ),
							$product->get_name()
						),
						403
					);
				}

				// If stock management is off, no need to reserve any stock here.
				if ( ! $product->managing_stock() || $product->backorders_allowed() ) {
					continue;
				}

				$managed_by_id = $product->get_stock_managed_by_id();

				/**
				 * Filter order item quantity.
				 *
				 * @param int|float             $quantity Quantity.
				 * @param WC_Order              $order    Order data.
				 * @param WC_Order_Item_Product $item Order item data.
				 */
				$item_quantity = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item );

				$rows[ $managed_by_id ] = isset( $rows[ $managed_by_id ] ) ? $rows[ $managed_by_id ] + $item_quantity : $item_quantity;
			}

			if ( ! empty( $rows ) ) {
				foreach ( $rows as $product_id => $quantity ) {
					$this->reserve_stock_for_product( $product_id, $quantity, $order, $minutes );
				}
			}
		} catch ( ReserveStockException $e ) {
			$this->release_stock_for_order( $order );
			throw $e;
		}
	}

	/**
	 * Release a temporary hold on stock for an order.
	 *
	 * @param \WC_Order $order Order object.
	 */
	public function release_stock_for_order( $order ) {
		global $wpdb;

		if ( ! $this->is_enabled() ) {
			return;
		}

		$wpdb->delete(
			$wpdb->wc_reserved_stock,
			array(
				'order_id' => $order->get_id(),
			)
		);
	}

	/**
	 * Reserve stock for a product by inserting rows into the DB.
	 *
	 * @throws ReserveStockException If a row cannot be inserted.
	 *
	 * @param int       $product_id Product ID which is having stock reserved.
	 * @param int       $stock_quantity Stock amount to reserve.
	 * @param \WC_Order $order Order object which contains the product.
	 * @param int       $minutes How long to reserve stock in minutes.
	 */
	private function reserve_stock_for_product( $product_id, $stock_quantity, $order, $minutes ) {
		global $wpdb;

		$product_data_store       = \WC_Data_Store::load( 'product' );
		$query_for_stock          = $product_data_store->get_query_for_stock( $product_id );
		$query_for_reserved_stock = $this->get_query_for_reserved_stock( $product_id, $order->get_id() );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
		$result = $wpdb->query(
			$wpdb->prepare(
				"
				INSERT INTO {$wpdb->wc_reserved_stock} ( `order_id`, `product_id`, `stock_quantity`, `timestamp`, `expires` )
				SELECT %d, %d, %d, NOW(), ( NOW() + INTERVAL %d MINUTE ) FROM DUAL
				WHERE ( $query_for_stock FOR UPDATE ) - ( $query_for_reserved_stock FOR UPDATE ) >= %d
				ON DUPLICATE KEY UPDATE `expires` = VALUES( `expires` ), `stock_quantity` = VALUES( `stock_quantity` )
				",
				$order->get_id(),
				$product_id,
				$stock_quantity,
				$minutes,
				$stock_quantity
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared

		if ( ! $result ) {
			$product = wc_get_product( $product_id );
			throw new ReserveStockException(
				'woocommerce_product_not_enough_stock',
				sprintf(
					/* translators: %s: product name */
					__( 'Not enough units of %s are available in stock to fulfil this order.', 'woocommerce' ),
					$product ? $product->get_name() : '#' . $product_id
				),
				403
			);
		}
	}

	/**
	 * Returns query statement for getting reserved stock of a product.
	 *
	 * @param int     $product_id Product ID.
	 * @param integer $exclude_order_id Optional order to exclude from the results.
	 * @return string|void Query statement.
	 */
	private function get_query_for_reserved_stock( $product_id, $exclude_order_id = 0 ) {
		global $wpdb;
		$query = $wpdb->prepare(
			"
			SELECT COALESCE( SUM( stock_table.`stock_quantity` ), 0 ) FROM $wpdb->wc_reserved_stock stock_table
			LEFT JOIN $wpdb->posts posts ON stock_table.`order_id` = posts.ID
			WHERE posts.post_status IN ( 'wc-checkout-draft', 'wc-pending' )
			AND stock_table.`expires` > NOW()
			AND stock_table.`product_id` = %d
			AND stock_table.`order_id` != %d
			",
			$product_id,
			$exclude_order_id
		);

		/**
		 * Filter: woocommerce_query_for_reserved_stock
		 * Allows to filter the query for getting reserved stock of a product.
		 *
		 * @since 4.5.0
		 * @param string $query            The query for getting reserved stock of a product.
		 * @param int    $product_id       Product ID.
		 * @param int    $exclude_order_id Order to exclude from the results.
		 */
		return apply_filters( 'woocommerce_query_for_reserved_stock', $query, $product_id, $exclude_order_id );
	}
}
Checkout/Helpers/ReserveStockException.php000064400000002311152333011040014707 0ustar00<?php
/**
 * Exceptions for stock reservation.
 */

namespace Automattic\WooCommerce\Checkout\Helpers;

defined( 'ABSPATH' ) || exit;

/**
 * ReserveStockException class.
 */
class ReserveStockException extends \Exception {
	/**
	 * Sanitized error code.
	 *
	 * @var string
	 */
	protected $error_code;

	/**
	 * Error extra data.
	 *
	 * @var array
	 */
	protected $error_data;

	/**
	 * Setup exception.
	 *
	 * @param string $code             Machine-readable error code, e.g `woocommerce_invalid_product_id`.
	 * @param string $message          User-friendly translated error message, e.g. 'Product ID is invalid'.
	 * @param int    $http_status_code Proper HTTP status code to respond with, e.g. 400.
	 * @param array  $data             Extra error data.
	 */
	public function __construct( $code, $message, $http_status_code = 400, $data = array() ) {
		$this->error_code = $code;
		$this->error_data = $data;

		parent::__construct( $message, $http_status_code );
	}

	/**
	 * Returns the error code.
	 *
	 * @return string
	 */
	public function getErrorCode() {
		return $this->error_code;
	}

	/**
	 * Returns error data.
	 *
	 * @return array
	 */
	public function getErrorData() {
		return $this->error_data;
	}
}