ampache/ampache

View on GitHub
src/Module/Application/Login/DefaultAction.php

Summary

Maintainability
D
2 days
Test Coverage
<?php

/**
 * vim:set softtabstop=4 shiftwidth=4 expandtab:
 *
 * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later)
 * Copyright Ampache.org, 2001-2023
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

namespace Ampache\Module\Application\Login;

use Ampache\Config\AmpConfig;
use Ampache\Config\ConfigContainerInterface;
use Ampache\Config\ConfigurationKeyEnum;
use Ampache\Module\User\Tracking\UserTrackerInterface;
use Ampache\Module\Util\RequestParserInterface;
use Ampache\Module\Util\UiInterface;
use Ampache\Repository\Model\Preference;
use Ampache\Repository\Model\User;
use Ampache\Module\Application\ApplicationActionInterface;
use Ampache\Module\Application\Exception\AccessDeniedException;
use Ampache\Module\Authorization\AccessLevelEnum;
use Ampache\Module\Authorization\Check\NetworkCheckerInterface;
use Ampache\Module\Authorization\GuiGatekeeperInterface;
use Ampache\Module\Authentication\AuthenticationManagerInterface;
use Ampache\Module\System\AmpError;
use Ampache\Module\System\AutoUpdate;
use Ampache\Module\System\Core;
use Ampache\Module\System\LegacyLogger;
use Ampache\Module\System\Session;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Teapot\StatusCode;

final class DefaultAction implements ApplicationActionInterface
{
    public const REQUEST_KEY = 'default';

    private RequestParserInterface $requestParser;

    private ConfigContainerInterface $configContainer;

    private AuthenticationManagerInterface $authenticationManager;

    private ResponseFactoryInterface $responseFactory;

    private LoggerInterface $logger;

    private NetworkCheckerInterface $networkChecker;

    private UiInterface $ui;

    private UserTrackerInterface $userTracker;

    public function __construct(
        RequestParserInterface $requestParser,
        ConfigContainerInterface $configContainer,
        AuthenticationManagerInterface $authenticationManager,
        ResponseFactoryInterface $responseFactory,
        LoggerInterface $logger,
        NetworkCheckerInterface $networkChecker,
        UiInterface $ui,
        UserTrackerInterface $userTracker
    ) {
        $this->requestParser         = $requestParser;
        $this->configContainer       = $configContainer;
        $this->authenticationManager = $authenticationManager;
        $this->responseFactory       = $responseFactory;
        $this->logger                = $logger;
        $this->networkChecker        = $networkChecker;
        $this->ui                    = $ui;
        $this->userTracker           = $userTracker;
    }

    public function run(ServerRequestInterface $request, GuiGatekeeperInterface $gatekeeper): ?ResponseInterface
    {
        // Avoid form login if still connected
        if ($this->configContainer->get('use_auth') && !isset($_GET['force_display'])) {
            $auth = false;
            $name = $this->configContainer->getSessionName();
            if (array_key_exists($name, $_COOKIE) && Session::exists('interface', $_COOKIE[$this->configContainer->getSessionName()])) {
                $auth = true;
            } elseif (Session::auth_remember()) {
                $auth = true;
            }
            if ($auth) {
                return $this->responseFactory
                    ->createResponse(StatusCode::FOUND)
                    ->withHeader(
                        'Location',
                        $this->configContainer->get('web_path')
                    );
            } elseif (array_key_exists($name, $_COOKIE)) {
                // now auth so unset this cookie
                setcookie($name, '', -1, (string)AmpConfig::get('cookie_path'));
                setcookie($name, '', -1);
            }
        }

        Session::create_cookie();
        Preference::init();

        /**
         * If Access Control is turned on then we don't
         * even want them to be able to get to the login
         * page if they aren't in the ACL
         */
        if ($this->configContainer->isFeatureEnabled(ConfigurationKeyEnum::ACCESS_CONTROL)) {
            if (!$this->networkChecker->check(AccessLevelEnum::TYPE_INTERFACE, null, AccessLevelEnum::LEVEL_GUEST)) {
                throw new AccessDeniedException(
                    sprintf(
                        'Access denied: %s is not in the Interface Access list',
                        Core::get_user_ip()
                    )
                );
            }
        } // access_control is enabled

        /* Clean Auth values */
        unset($auth);

        if (empty($this->requestParser->getFromRequest('step'))) {
            /* Check for posted username and password, or appropriate environment variable if using HTTP auth */
            if (
                (isset($_POST['username'])) ||
                (in_array('http', $this->configContainer->get(ConfigurationKeyEnum::AUTH_METHODS)) && (isset($_SERVER['REMOTE_USER']) || isset($_SERVER['HTTP_REMOTE_USER'])))
            ) {
                /* If we are in demo mode let's force auth success */
                if ($this->configContainer->isFeatureEnabled(ConfigurationKeyEnum::DEMO_MODE) === true) {
                    $auth                         = array();
                    $auth['success']              = true;
                    $auth['info']['username']     = 'Admin - DEMO';
                    $auth['info']['fullname']     = 'Administrative User';
                    $auth['info']['offset_limit'] = 50;
                } else {
                    if (Core::get_post('username') !== '') {
                        $username = (string)$_POST['username'];
                        $password = $_POST['password'] ?? '';
                    } else {
                        if (isset($_SERVER['REMOTE_USER'])) {
                            $username = (string) Core::get_server('REMOTE_USER');
                        } elseif (isset($_SERVER['HTTP_REMOTE_USER'])) {
                            $username = (string) Core::get_server('HTTP_REMOTE_USER');
                        } else {
                            $username = '';
                        }
                        $password = '';
                    }

                    $auth = $this->authenticationManager->login($username, $password, true);

                    if ($auth['success']) {
                        $username = $auth['username'];
                    } elseif (array_key_exists('ui_required', $auth)) {
                        echo $auth['ui_required'];

                        return null;
                    } else {
                        $this->logger->warning(
                            sprintf(
                                '%s From %s attempted to login and failed',
                                scrub_out($username),
                                Core::get_user_ip()
                            ),
                            [LegacyLogger::CONTEXT_TYPE => __CLASS__]
                        );
                        AmpError::add('general', T_('Incorrect username or password'));
                    }
                }
            }
        } elseif ($this->requestParser->getFromRequest('step') == '2') {
            $auth_mod = $this->requestParser->getFromRequest('auth_mod');

            $auth = $this->authenticationManager->postAuth($auth_mod);

            /**
             * postAuth may return null, so this has to be considered in here
             */
            if (isset($auth['success']) && $auth['success']) {
                $username = $auth['username'];
            } else {
                $this->logger->error(
                    'Second step authentication failed',
                    [LegacyLogger::CONTEXT_TYPE => __CLASS__]
                );
                AmpError::add('general', $auth['error'] ?? '');
            }
        }

        $user = null;
        if (!empty($username) && isset($auth)) {
            $user = User::get_from_username($username);

            if ($user instanceof User && $user->disabled) {
                // if user disabled
                $auth['success'] = false;
                AmpError::add('general', T_('Account is disabled, please contact the administrator'));
                $this->logger->warning(
                    sprintf('%s is disabled and attempted to login', scrub_out($username)),
                    [LegacyLogger::CONTEXT_TYPE => __CLASS__]
                );
            } elseif (AmpConfig::get('prevent_multiple_logins')) {
                // if logged in multiple times
                $session_ip = ($user instanceof User) ? $user->is_logged_in() : false;
                $current_ip = Core::get_user_ip();
                if ($current_ip && ($current_ip != $session_ip)) {
                    $auth['success'] = false;
                    AmpError::add('general', T_('User is already logged in'));

                    $this->logger->notice(
                        sprintf(
                            '%s is already logged in from %s and attempted to login from %s',
                            scrub_out($username),
                            (string) $session_ip,
                            $current_ip
                        ),
                        [LegacyLogger::CONTEXT_TYPE => __CLASS__]
                    );
                }
            } elseif ($this->configContainer->isFeatureEnabled(ConfigurationKeyEnum::AUTO_CREATE) && $auth['success'] && !$user instanceof User) {
                // This is run if we want to autocreate users who don't exist (useful for non-mysql auth)
                $access   = User::access_name_to_level($this->configContainer->get(ConfigurationKeyEnum::AUTO_USER) ?? 'guest');
                $fullname = array_key_exists('name', $auth) ? $auth['name'] : '';
                $email    = array_key_exists('email', $auth) ? $auth['email'] : '';
                $website  = array_key_exists('website', $auth) ? $auth['website'] : '';
                $state    = array_key_exists('state', $auth) ? $auth['state'] : '';
                $city     = array_key_exists('city', $auth) ? $auth['city'] : '';
                $dfg      = array_key_exists('catalog_filter_group', $auth) ? $auth['catalog_filter_group'] : 0;

                // Attempt to create the user
                $user_id = User::create($username, $fullname, $email, $website, hash('sha256', bin2hex(random_bytes(20))), $access, $dfg, $state, $city);
                if ($user_id > 0) {
                    // tell me you're creating the user
                    $this->logger->notice(
                        sprintf(
                            'Created missing user %s',
                            scrub_out($username)
                        ),
                        [LegacyLogger::CONTEXT_TYPE => __CLASS__]
                    );
                    $user = new User($user_id);

                    if (array_key_exists('avatar', $auth)) {
                        $user->update_avatar($auth['avatar']['data'], $auth['avatar']['mime']);
                    }
                } else {
                    $auth['success'] = false;
                    $this->logger->error(
                        'Unable to create a local account',
                        [LegacyLogger::CONTEXT_TYPE => __CLASS__]
                    );
                    AmpError::add('general', T_('Unable to create a local account'));
                }
            } // end if auto_create

            // This allows stealing passwords validated by external means such as LDAP
            if (
                $this->configContainer->isFeatureEnabled(ConfigurationKeyEnum::AUTH_PASSWORD_SAVE) &&
                isset($auth) &&
                $auth['success'] &&
                isset($password) &&
                $user instanceof User
            ) {
                $user->update_password($password);
            }
        }

        /* If the authentication was a success */
        if (isset($auth) && $auth['success'] && $user instanceof User) {
            // $auth->info are the fields specified in the config file
            //   to retrieve for each user
            Session::create($auth);

            // Not sure if it was me or php tripping out, but naming this 'user' didn't work at all
            $_SESSION['userdata'] = $auth;

            // You really don't want to store the avatar
            //   in the SESSION.
            unset($_SESSION['userdata']['avatar']);

            // Record the IP of this person!
            $this->userTracker->trackIpAddress($user);

            if (isset($username)) {
                Session::create_user_cookie($username);
                if (isset($_POST['rememberme'])) {
                    Session::create_remember_cookie($username);
                }
            }

            // Update data from this auth if ours are empty or if config asks us to
            $external_auto_update = $this->configContainer->isFeatureEnabled(ConfigurationKeyEnum::EXTERNAL_AUTO_UPDATE);

            if (($external_auto_update || empty($user->fullname)) && !empty($auth['name'])) {
                $user->update_fullname($auth['name']);
            }
            if (($external_auto_update || empty($user->email)) && !empty($auth['email'])) {
                $user->update_email($auth['email']);
            }
            if (($external_auto_update || empty($user->website)) && !empty($auth['website'])) {
                $user->update_website($auth['website']);
            }
            if (($external_auto_update || empty($user->state)) && !empty($auth['state'])) {
                $user->update_state($auth['state']);
            }
            if (($external_auto_update || empty($user->city)) && !empty($auth['city'])) {
                $user->update_city($auth['city']);
            }
            if (($external_auto_update || empty($user->f_avatar)) && !empty($auth['avatar'])) {
                $user->update_avatar($auth['avatar']['data'], $auth['avatar']['mime']);
            }

            Session::createGlobalUser($user);
            // If an admin, check for update
            if (
                $this->configContainer->isFeatureEnabled(ConfigurationKeyEnum::AUTOUPDATE) &&
                $gatekeeper->mayAccess(AccessLevelEnum::TYPE_INTERFACE, AccessLevelEnum::LEVEL_ADMIN)
            ) {
                // admins need to know if an update is available
                AutoUpdate::is_update_available();
                // Make sure all default preferences are set
                Preference::set_defaults();
            }
            // fix preferences that are missing for user
            User::fix_preferences($user->id);

            /* Make sure they are actually trying to get to this site and don't try
             * to redirect them back into an admin section
             */
            $web_path = $this->configContainer->getWebPath();
            if (
                (substr($_POST['referrer'], 0, strlen((string) $web_path)) == $web_path) &&
                strpos($_POST['referrer'], 'install.php') === false &&
                strpos($_POST['referrer'], 'login.php') === false &&
                strpos($_POST['referrer'], 'logout.php') === false &&
                strpos($_POST['referrer'], 'update.php') === false &&
                strpos($_POST['referrer'], 'activate.php') === false &&
                strpos($_POST['referrer'], 'admin') === false
            ) {
                return $this->responseFactory
                    ->createResponse(StatusCode::FOUND)
                    ->withHeader(
                        'Location',
                        $_POST['referrer']
                    );
            } // if we've got a referrer

            return $this->responseFactory
                ->createResponse(StatusCode::FOUND)
                ->withHeader(
                    'Location',
                    sprintf('%s/index.php', $this->configContainer->getWebPath())
                );
        } // auth success

        $this->ui->show('show_login_form.inc.php');

        return null;
    }
}