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

/**
 * Tests for core_message_inbound to test Variable Envelope Return Path functionality.
 *
 * @package    core_message
 * @copyright  2014 Andrew Nicols <andrew@nicols.co.uk>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace core_message;

defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/inbound_fixtures.php');

/**
 * Tests for core_message_inbound to test Variable Envelope Return Path functionality.
 *
 * @package    core_message
 * @copyright  2014 Andrew Nicols <andrew@nicols.co.uk>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
final class inbound_test extends \advanced_testcase {

    /**
     * Perform setup tasks generic to each test.
     * This includes:
     * * configuring the messageinbound_mailbox.
     */
    public function setUp(): void {
        global $CFG;
        parent::setUp();

        $this->resetAfterTest(true);

        // Setup the default Inbound Message mailbox settings.
        $CFG->messageinbound_domain = 'example.com';
        $CFG->messageinbound_enabled = true;

        // Must be no longer than 15 characters.
        $CFG->messageinbound_mailbox = 'moodlemoodle123';
    }

    /**
     * Helper to create a new Inbound Message handler.
     *
     * @param $handlerclass The class of the handler to create
     * @param $enabled Whether the handler should be enabled
     * @param $component The component
     * @param $namepace The namepace
     */
    public function helper_create_handler($handlerclass, $enabled = true, $component = 'core_test', $namespace = '\\core\\test\\') {
        global $DB;

        $classname = $namespace . $handlerclass;
        $record = \core\message\inbound\manager::record_from_handler(new $classname());
        $record->component = $component;
        $record->enabled = $enabled;
        $record->id = $DB->insert_record('messageinbound_handlers', $record);
        $handler = core_message_inbound_test_manager::handler_from_record($record);

        return $handler;
    }

    /**
     * Test that the enabled check perform as expected.
     */
    public function test_is_enabled(): void {
        global $CFG;

        // First clear all of the settings set in the setUp.
        $CFG->messageinbound_domain = null;
        $CFG->messageinbound_enabled = null;
        $CFG->messageinbound_mailbox = null;

        $this->assertFalse(\core\message\inbound\manager::is_enabled());

        // Check whether only setting the enabled flag keeps it disabled.
        $CFG->messageinbound_enabled = true;
        $this->assertFalse(\core\message\inbound\manager::is_enabled());

        // Check that the mailbox entry on it's own does not enable Inbound Message handling.
        $CFG->messageinbound_mailbox = 'moodlemoodle123';
        $CFG->messageinbound_domain = null;
        $this->assertFalse(\core\message\inbound\manager::is_enabled());

        // And that the domain on it's own does not.
        $CFG->messageinbound_domain = 'example.com';
        $CFG->messageinbound_mailbox = null;
        $this->assertFalse(\core\message\inbound\manager::is_enabled());

        // And that an invalid mailbox does not.
        $CFG->messageinbound_mailbox = '';
        $CFG->messageinbound_domain = 'example.com';
        $this->assertFalse(\core\message\inbound\manager::is_enabled());

        // And that an invalid domain does not.
        $CFG->messageinbound_domain = '';
        $CFG->messageinbound_mailbox = 'moodlemoodle123';
        $this->assertFalse(\core\message\inbound\manager::is_enabled());

        // Finally a test that ensures that all settings correct enables the system.
        $CFG->messageinbound_mailbox = 'moodlemoodle123';
        $CFG->messageinbound_domain = 'example.com';
        $CFG->messageinbound_enabled = true;

        $this->assertTrue(\core\message\inbound\manager::is_enabled());
    }

    /**
     * Test that data items conform to RFCs 5231, and 5322 standards for
     * addressing, and to RFC 5233 for sub-addressing.
     */
    public function test_address_constraints(): void {
        $handler = $this->helper_create_handler('handler_one');

        // Using the handler created, generate an address for our data entry.
        $processor = new core_message_inbound_test_helper();
        $processor->set_handler($handler->classname);

        // Generate some IDs for the data and generate addresses for them.
        $dataids = array(
            -1,
            0,
            42,
            1073741823,
            2147483647,
        );

        $user = $this->getDataGenerator()->create_user();
        foreach ($dataids as $dataid) {
            $processor->set_data($dataid);
            $address = $processor->generate($user->id);
            $this->assertNotNull($address);
            $this->assertTrue(strlen($address) > 0, 'No address generated.');
            $this->assertTrue(strpos($address, '@') !== false, 'No domain found.');
            $this->assertTrue(strpos($address, '+') !== false, 'No subaddress found.');

            // The localpart must be less than 64 characters.
            list($localpart) = explode('@', $address);
            $this->assertTrue(strlen($localpart) <= 64, 'Localpart section of address too long');

            // And the data section should be no more than 48 characters.
            list(, $datasection) = explode('+', $localpart);
            $this->assertTrue(strlen($datasection) <= 48, 'Data section of address too long');
        }
    }

    /**
     * Test that the generated e-mail addresses are sufficiently random by
     * testing the multiple handlers, multiple users, and multiple data
     * items.
     */
    public function test_address_uniqueness(): void {
        // Generate a set of handlers. These are in two components, and each
        // component has two different generators.
        $handlers = array();
        $handlers[] = $this->helper_create_handler('handler_one', true, 'core_test');
        $handlers[] = $this->helper_create_handler('handler_two', true, 'core_test');
        $handlers[] = $this->helper_create_handler('handler_three', true, 'core_test_example');
        $handlers[] = $this->helper_create_handler('handler_four', true, 'core_test_example');

        // Generate some IDs for the data and generate addresses for them.
        $dataids = array(
            0,
            42,
            1073741823,
            2147483647,
        );

        $users = array();
        for ($i = 0; $i < 5; $i++) {
            $users[] = $this->getDataGenerator()->create_user();
        }

        // Store the addresses for later comparison.
        $addresses = array();

        foreach ($handlers as $handler) {
            $processor = new core_message_inbound_test_helper();
            $processor->set_handler($handler->classname);

            // Check each dataid.
            foreach ($dataids as $dataid) {
                $processor->set_data($dataid);

                // Check each user.
                foreach ($users as $user) {
                    $address = $processor->generate($user->id);
                    $this->assertFalse(isset($addresses[$address]));
                    $addresses[$address] = true;
                }
            }
        }
    }

    /**
     * Test address parsing of a generated address.
     */
    public function test_address_parsing(): void {
        $dataid = 42;

        // Generate a handler to use for this set of tests.
        $handler = $this->helper_create_handler('handler_one');

        // And a user.
        $user = $this->getDataGenerator()->create_user();

        // Using the handler created, generate an address for our data entry.
        $processor = new core_message_inbound_test_helper();
        $processor->set_handler($handler->classname);
        $processor->set_data($dataid);
        $address = $processor->generate($user->id);

        // We should be able to parse the address.
        $parser = new core_message_inbound_test_helper();
        $parser->process($address);
        $parsedresult = $parser->get_data();
        $this->assertEquals($user->id, $parsedresult->userid);
        $this->assertEquals($dataid, $parsedresult->datavalue);
        $this->assertEquals($dataid, $parsedresult->data->datavalue);
        $this->assertEquals($handler->id, $parsedresult->handlerid);
        $this->assertEquals($handler->id, $parsedresult->data->handler);
    }

    /**
     * Test address parsing of an address with an unrecognised format.
     */
    public function test_address_validation_invalid_format_failure(): void {
        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');
        $dataid = 42;

        $parser = new core_message_inbound_test_helper();

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);

        // Check that validation fails when no address has been processed.
        $result = $parser->validate($user->email);
        $this->assertEquals(\core\message\inbound\address_manager::VALIDATION_INVALID_ADDRESS_FORMAT, $result);

        // Test that an address without data fails validation.
        $parser->process('bob@example.com');
        $result = $parser->validate($user->email);
        $this->assertEquals(\core\message\inbound\address_manager::VALIDATION_INVALID_ADDRESS_FORMAT, $result);

        // Test than address with a subaddress but invalid data fails with VALIDATION_UNKNOWN_DATAKEY.
        $parser->process('bob+nodata@example.com');
        $result = $parser->validate($user->email);
        $this->assertEquals(\core\message\inbound\address_manager::VALIDATION_INVALID_ADDRESS_FORMAT, $result);
    }

    /**
     * Test address parsing of an address with an unknown handler.
     */
    public function test_address_validation_unknown_handler(): void {
        global $DB;

        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');
        $dataid = 42;

        $parser = new core_message_inbound_test_helper();

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);
        $generator->set_data($dataid);
        $address = $generator->generate($user->id);

        // Remove the handler record to invalidate it.
        $DB->delete_records('messageinbound_handlers', array(
            'id' => $handler->id,
        ));

        $parser->process($address);
        $result = $parser->validate($user->email);
        $expectedfail = \core\message\inbound\address_manager::VALIDATION_UNKNOWN_HANDLER;
        $this->assertEquals($expectedfail, $result & $expectedfail);
    }

    /**
     * Test address parsing of an address with a disabled handler.
     */
    public function test_address_validation_disabled_handler(): void {
        global $DB;

        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');
        $dataid = 42;

        $parser = new core_message_inbound_test_helper();

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);
        $generator->set_data($dataid);
        $address = $generator->generate($user->id);

        // Disable the handler.
        $record = \core\message\inbound\manager::record_from_handler($handler);
        $record->enabled = false;
        $DB->update_record('messageinbound_handlers', $record);

        $parser->process($address);
        $result = $parser->validate($user->email);
        $expectedfail = \core\message\inbound\address_manager::VALIDATION_DISABLED_HANDLER;
        $this->assertEquals($expectedfail, $result & $expectedfail);
    }

    /**
     * Test address parsing of an address for an invalid user.
     */
    public function test_address_validation_invalid_user(): void {
        global $DB;

        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');
        $dataid = 42;

        $parser = new core_message_inbound_test_helper();

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);
        $generator->set_data($dataid);
        $address = $generator->generate(-1);

        $parser->process($address);
        $result = $parser->validate($user->email);
        $expectedfail = \core\message\inbound\address_manager::VALIDATION_UNKNOWN_USER;
        $this->assertEquals($expectedfail, $result & $expectedfail);
    }

    /**
     * Test address parsing of an address for a disabled user.
     */
    public function test_address_validation_disabled_user(): void {
        global $DB;

        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');
        $dataid = 42;

        $parser = new core_message_inbound_test_helper();

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);
        $generator->set_data($dataid);
        $address = $generator->generate($user->id);

        // Unconfirm the user.
        $user->confirmed = 0;
        $DB->update_record('user', $user);

        $parser->process($address);
        $result = $parser->validate($user->email);
        $expectedfail = \core\message\inbound\address_manager::VALIDATION_DISABLED_USER;
        $this->assertEquals($expectedfail, $result & $expectedfail);
    }

    /**
     * Test address parsing of an address for an invalid key.
     */
    public function test_address_validation_invalid_key(): void {
        global $DB;

        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');
        $dataid = 42;

        $parser = new core_message_inbound_test_helper();

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);
        $generator->set_data($dataid);
        $address = $generator->generate($user->id);

        // Remove the data record to invalidate it.
        $DB->delete_records('messageinbound_datakeys', array(
            'handler' => $handler->id,
            'datavalue' => $dataid,
        ));

        $parser->process($address);
        $result = $parser->validate($user->email);
        $expectedfail = \core\message\inbound\address_manager::VALIDATION_UNKNOWN_DATAKEY;
        $this->assertEquals($expectedfail, $result & $expectedfail);
    }

    /**
     * Test address parsing of an address for an expired key.
     */
    public function test_address_validation_expired_key(): void {
        global $DB;

        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');
        $dataid = 42;

        $parser = new core_message_inbound_test_helper();

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);
        $generator->set_data($dataid);
        $address = $generator->generate($user->id);

        // Expire the key by setting it's expiry time in the past.
        $key = $DB->get_record('messageinbound_datakeys', array(
            'handler' => $handler->id,
            'datavalue' => $dataid,
        ));

        $key->expires = time() - 3600;
        $DB->update_record('messageinbound_datakeys', $key);

        $parser->process($address);
        $result = $parser->validate($user->email);
        $expectedfail = \core\message\inbound\address_manager::VALIDATION_EXPIRED_DATAKEY;
        $this->assertEquals($expectedfail, $result & $expectedfail);
    }

    /**
     * Test address parsing of an address for an invalid hash.
     */
    public function test_address_validation_invalid_hash(): void {
        global $DB;

        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');
        $dataid = 42;

        $parser = new core_message_inbound_test_helper();

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);
        $generator->set_data($dataid);
        $address = $generator->generate($user->id);

        // Expire the key by setting it's expiry time in the past.
        $key = $DB->get_record('messageinbound_datakeys', array(
            'handler' => $handler->id,
            'datavalue' => $dataid,
        ));

        $key->datakey = 'invalid value';
        $DB->update_record('messageinbound_datakeys', $key);

        $parser->process($address);
        $result = $parser->validate($user->email);
        $expectedfail = \core\message\inbound\address_manager::VALIDATION_INVALID_HASH;
        $this->assertEquals($expectedfail, $result & $expectedfail);
    }

    /**
     * Test address parsing of an address for an invalid sender.
     */
    public function test_address_validation_invalid_sender(): void {
        global $DB;

        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');
        $dataid = 42;

        $parser = new core_message_inbound_test_helper();

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);
        $generator->set_data($dataid);
        $address = $generator->generate($user->id);

        $parser->process($address);
        $result = $parser->validate('incorrectuser@example.com');
        $expectedfail = \core\message\inbound\address_manager::VALIDATION_ADDRESS_MISMATCH;
        $this->assertEquals($expectedfail, $result & $expectedfail);
    }

    /**
     * Test address parsing of an address for an address which is correct.
     */
    public function test_address_validation_success(): void {
        global $DB;

        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');
        $dataid = 42;

        $parser = new core_message_inbound_test_helper();

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);
        $generator->set_data($dataid);
        $address = $generator->generate($user->id);

        $parser->process($address);
        $result = $parser->validate($user->email);
        $this->assertEquals(\core\message\inbound\address_manager::VALIDATION_SUCCESS, $result);

    }

    /**
     * Test that a handler with no default expiration does not have an
     * expiration time applied.
     */
    public function test_default_hander_expiry_unlimited(): void {
        global $DB;

        // Set the default expiry of the handler to 0 - no expiration.
        $expiration = 0;

        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');

        $record = \core\message\inbound\manager::record_from_handler($handler);
        $record->defaultexpiration = $expiration;
        $DB->update_record('messageinbound_handlers', $record);

        // Generate an address for the handler.
        $dataid = 42;

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);
        $generator->set_data($dataid);
        $address = $generator->generate($user->id);

        // Check that the datakey created matches the expirytime.
        $key = $DB->get_record('messageinbound_datakeys', array('handler' => $record->id, 'datavalue' => $dataid));

        $this->assertNull($key->expires);
    }

    /**
     * Test application of the default expiry on a handler.
     */
    public function test_default_hander_expiry_low(): void {
        global $DB;

        // Set the default expiry of the handler to 60 seconds.
        $expiration = 60;

        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');

        $record = \core\message\inbound\manager::record_from_handler($handler);
        $record->defaultexpiration = $expiration;
        $DB->update_record('messageinbound_handlers', $record);

        // Generate an address for the handler.
        $dataid = 42;

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);
        $generator->set_data($dataid);
        $address = $generator->generate($user->id);

        // Check that the datakey created matches the expirytime.
        $key = $DB->get_record('messageinbound_datakeys', array('handler' => $record->id, 'datavalue' => $dataid));

        $this->assertEquals($key->timecreated + $expiration, $key->expires);
    }

    /**
     * Test application of the default expiry on a handler.
     */
    public function test_default_hander_expiry_medium(): void {
        global $DB;

        // Set the default expiry of the handler to 3600 seconds.
        $expiration = 3600;

        // Create test data.
        $user = $this->getDataGenerator()->create_user();
        $handler = $this->helper_create_handler('handler_one');

        $record = \core\message\inbound\manager::record_from_handler($handler);
        $record->defaultexpiration = $expiration;
        $DB->update_record('messageinbound_handlers', $record);

        // Generate an address for the handler.
        $dataid = 42;

        $generator = new core_message_inbound_test_helper();
        $generator->set_handler($handler->classname);
        $generator->set_data($dataid);
        $address = $generator->generate($user->id);

        // Check that the datakey created matches the expirytime.
        $key = $DB->get_record('messageinbound_datakeys', array('handler' => $record->id, 'datavalue' => $dataid));

        $this->assertEquals($key->timecreated + $expiration, $key->expires);
    }

}

/**
 * A helper function for unit testing to expose protected functions in the core_message_inbound API for testing.
 *
 * @copyright  2014 Andrew Nicols <andrew@nicols.co.uk>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class core_message_inbound_test_helper extends \core\message\inbound\address_manager {
    /**
     * The validate function.
     *
     * @param string $address
     * @return int
     */
    public function validate($address) {
        return parent::validate($address);
    }

    /**
     * The get_data function.
     *
     * @return stdClass
     */
    public function get_data() {
        return parent::get_data();
    }

    /**
     * The address processor function.
     *
     * @param string $address
     * @return void
     */
    public function process($address) {
        return parent::process($address);
    }
}

/**
 * A helper function for unit testing to expose protected functions in the core_message_inbound API for testing.
 *
 * @copyright  2014 Andrew Nicols <andrew@nicols.co.uk>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class core_message_inbound_test_manager extends \core\message\inbound\manager {
    /**
     * Helper to fetch make the handler_from_record public for unit testing.
     *
     * @param $record The handler record to fetch
     */
    public static function handler_from_record($record) {
        return parent::handler_from_record($record);
    }
}
