<?php
/**
 * Creates a bot password for an existing user account.
 *
 * @license GPL-2.0-or-later
 * @file
 * @ingroup Maintenance
 * @author Alex Dean <wikimedia@mostlyalex.com>
 */

use MediaWiki\Maintenance\Maintenance;
use MediaWiki\User\BotPassword;
use MediaWiki\User\User;

// @codeCoverageIgnoreStart
require_once __DIR__ . '/Maintenance.php';
// @codeCoverageIgnoreEnd

class CreateBotPassword extends Maintenance {
	/**
	 * Width of initial column of --showgrants output
	 */
	private const SHOWGRANTS_COLUMN_WIDTH = 20;

	public function __construct() {
		parent::__construct();
		$this->addDescription(
			'Create a bot password for a user. ' .
			'See https://www.mediawiki.org/wiki/Manual:Bot_passwords for more information.'
		);

		$this->addOption( "showgrants",
			"Prints a description of available grants and exits."
		);
		$this->addOption( "appid",
			"App id for the new bot password.", false, true
		);
		$this->addOption( "grants",
			"CSV list of permissions to grant.", false, true
		);
		$this->addArg( "user",
			"The username to create a bot password for.", false
		);
		$this->addArg( "password",
			"A password will be generated if this is omitted." .
			" If supplied, it must be exactly 32 characters.", false
		);
	}

	public function execute() {
		if ( $this->hasOption( 'showgrants' ) ) {
			$this->showGrants();
			return;
		}

		$username = $this->getArg( 0 );
		$password = $this->getArg( 1 );
		$appId = $this->getOption( 'appid' );
		$grants = explode( ',', $this->getOption( 'grants', '' ) );

		$errors = [];
		if ( $username === null ) {
			$errors[] = "Argument <user> required!";
		}
		if ( $appId == null ) {
			$errors[] = "Param appid required!";
		}
		if ( $this->getOption( 'grants' ) === null ) {
			$errors[] = "Param grants required!";
		}
		if ( count( $errors ) > 0 ) {
			$this->fatalError( implode( "\n", $errors ) );
		}

		$services = $this->getServiceContainer();
		$grantsInfo = $services->getGrantsInfo();
		$invalidGrants = array_diff( $grants, $grantsInfo->getValidGrants() );
		if ( count( $invalidGrants ) > 0 ) {
			$this->fatalError(
				"These grants are invalid: " . implode( ', ', $invalidGrants ) . "\n" .
				"Use the --showgrants option for a full list of valid grant names."
			);
		}

		$passwordFactory = $services->getPasswordFactory();

		$userIdentity = $services->getUserIdentityLookup()->getUserIdentityByName( $username );
		if ( !$userIdentity || !$userIdentity->isRegistered() ) {
			$this->fatalError( "Cannot create bot password for non-existent user '$username'." );
		}

		if ( $password === null ) {
			$password = BotPassword::generatePassword();
		} else {
			$passwordLength = strlen( $password );
			if ( $passwordLength < BotPassword::PASSWORD_MINLENGTH ) {
				$message = "Bot passwords must have at least " . BotPassword::PASSWORD_MINLENGTH .
					" characters. Given password is $passwordLength characters.";
				$this->fatalError( $message );
			}
		}

		$bp = BotPassword::newUnsaved( [
			'username' => $username,
			'appId' => $appId,
			'grants' => $grants
		] );

		if ( $bp === null ) {
			$this->fatalError( "Bot password creation failed." );
		}

		$passwordInstance = $passwordFactory->newFromPlaintext( $password );
		$status = $bp->save( 'insert', $passwordInstance );

		if ( $status->isGood() ) {
			$this->output( "Success.\n" );
			$this->output( "Log in using username:'{$username}@{$appId}' and password:'{$password}'.\n" );
		} else {
			$this->error( "Bot password creation failed. Does this appid already exist for the user perhaps?" );
			$this->fatalError( $status );
		}
	}

	public function showGrants() {
		$permissions = $this->getServiceContainer()->getGrantsInfo()->getValidGrants();
		sort( $permissions );

		$this->output( str_pad( 'GRANT', self::SHOWGRANTS_COLUMN_WIDTH ) . " DESCRIPTION\n" );
		foreach ( $permissions as $permission ) {
			$this->output(
				str_pad( $permission, self::SHOWGRANTS_COLUMN_WIDTH ) . " " .
				User::getRightDescription( $permission ) . "\n"
			);
		}
	}
}

// @codeCoverageIgnoreStart
$maintClass = CreateBotPassword::class;
require_once RUN_MAINTENANCE_IF_MAIN;
// @codeCoverageIgnoreEnd
