<?php
/* vim:set softtabstop=4 shiftwidth=4 expandtab: */
/**
 *
 * LICENSE: GNU Affero General Public License, version 3 (AGPLv3)
 * Copyright 2001 - 2017 Ampache.org
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 */


/**
 *
 * This class handles all of the session related stuff in Ampache
 * it takes over for the vauth libs, and takes some stuff out of other
 * classes where it didn't belong.
 */
class Auth
{
    /**
     * Constructor
     *
     * This should never be called
     */
    private function __construct()
    {
        // Rien a faire
    }

    /**
     * logout
     *
     * This is called when you want to log out and nuke your session.
     * This is the function used for the Ajax logouts, if no id is passed
     * it tries to find one from the session,
     * @param string $key
     * @param boolean $relogin
     */
    public static function logout($key='', $relogin = true)
    {
        // If no key is passed try to find the session id
        $key = $key ? $key : session_id();

        // Nuke the cookie before all else
        Session::destroy($key);
        if ((!$relogin) && AmpConfig::get('logout_redirect')) {
            $target = AmpConfig::get('logout_redirect');
        } else {
            $target = AmpConfig::get('web_path') . '/login.php';
        }

        // Do a quick check to see if this is an AJAXed logout request
        // if so use the iframe to redirect
        if (defined('AJAX_INCLUDE')) {
            ob_end_clean();
            ob_start();

            xoutput_headers();

            $results            = array();
            $results['rfc3514'] = '<script type="text/javascript">reloadRedirect("' . $target . '")</script>';
            echo xoutput_from_array($results);
        } else {
            /* Redirect them to the login page */
            header('Location: ' . $target);
        }

        exit;
    }

    /**
     * login
     *
     * This takes a username and password and then returns the results
     * based on what happens when we try to do the auth.
     * @param string $username
     * @param string $password
     * @param boolean $allow_ui
     * @return array
     */
    public static function login($username, $password, $allow_ui = false)
    {
        $results = array();
        foreach (AmpConfig::get('auth_methods') as $method) {
            $function_name = $method . '_auth';

            if (!method_exists('Auth', $function_name)) {
                continue;
            }

            $results = self::$function_name($username, $password);
            if ($results['success'] || ($allow_ui && !empty($results['ui_required']))) {
                break;
            }
        }

        return $results;
    }

    /**
     * login_step2
     *
     * This process authenticate step2 for an auth module
     * @param string $auth_mod
     * @return array
     */
    public static function login_step2($auth_mod)
    {
        $results = null;
        if (in_array($auth_mod, AmpConfig::get('auth_methods'))) {
            $function_name = $auth_mod . '_auth_2';
            if (method_exists('Auth', $function_name)) {
                $results = self::$function_name();
            }
        }

        return $results;
    }

    /**
     * mysql_auth
     *
     * This is the core function of our built-in authentication.
     * @param string $username
     * @param string $password
     * @return array
     */
    private static function mysql_auth($username, $password)
    {
        if (strlen($password) && strlen($username)) {
            $sql        = 'SELECT `password` FROM `user` WHERE `username` = ?';
            $db_results = Dba::read($sql, array($username));

            if ($row = Dba::fetch_assoc($db_results)) {
                // Use SHA2 now... cooking with fire.
                // For backwards compatibility we hash a couple of different
                // variations of the password. Increases collision chances, but
                // doesn't break things.
                // FIXME: Break things in the future.
                $hashed_password   = array();
                $hashed_password[] = hash('sha256', $password);
                $hashed_password[] = hash('sha256',
                    Dba::escape(stripslashes(htmlspecialchars(strip_tags($password)))));

                // Automagically update the password if it's old and busted.
                if ($row['password'] == $hashed_password[1] &&
                    $hashed_password[0] != $hashed_password[1]) {
                    $user = User::get_from_username($username);
                    $user->update_password($password);
                }

                if (in_array($row['password'], $hashed_password)) {
                    return array(
                        'success' => true,
                        'type' => 'mysql',
                        'username' => $username
                    );
                }
            }
        }

        return array(
            'success' => false,
            'error' => 'MySQL login attempt failed'
        );
    }

    /**
     * pam_auth
     *
     * Check to make sure the pam_auth function is implemented (module is
     * installed), then check the credentials.
     * @param string $username
     * @param string $password
     * @return array
     */
    private static function pam_auth($username, $password)
    {
        $results = array();
        if (!function_exists('pam_auth')) {
            $results['success']    = false;
            $results['error']      = 'The PAM PHP module is not installed';

            return $results;
        }

        $password = scrub_in($password);

        if (pam_auth($username, $password)) {
            $results['success']     = true;
            $results['type']        = 'pam';
            $results['username']    = $username;
        } else {
            $results['success']    = false;
            $results['error']      = 'PAM login attempt failed';
        }

        return $results;
    }

    /**
     * external_auth
     *
     * Calls an external program compatible with mod_authnz_external
     * such as pwauth.
     * @param string $username
     * @param string $password
     * @return array
     */
    private static function external_auth($username, $password)
    {
        $authenticator = AmpConfig::get('external_authenticator');
        if (!$authenticator) {
            return array(
                'success' => false,
                'error' => 'No external authenticator configured'
            );
        }

        //FIXME: should we do input sanitization?
        $proc = proc_open($authenticator,
            array(
                0 => array('pipe', 'r'),
                1 => array('pipe', 'w'),
                2 => array('pipe', 'w')
            ), $pipes);

        if (is_resource($proc)) {
            fwrite($pipes[0], $username . "\n" . $password . "\n");
            fclose($pipes[0]);
            fclose($pipes[1]);
            if ($stderr = fread($pipes[2], 8192)) {
                debug_event('external_auth', "fread error: " . $stderr, 5);
            }
            fclose($pipes[2]);
        } else {
            return array(
                'success' => false,
                'error' => 'Failed to run external authenticator'
            );
        }

        if (proc_close($proc) == 0) {
            return array(
                'success' => true,
                'type' => 'external',
                'username' => $username
            );
        }

        return array(
            'success' => false,
            'error' => 'The external authenticator did not accept the login'
        );
    }

    /**
     * ldap_auth
     * @param string $username
     * @param string $password
     * @return array
     */
    private static function ldap_auth($username, $password)
    {
        return LDAP::auth($username, $password);
    }

    /**
     * http_auth
     * This auth method relies on HTTP auth from the webserver
     *
     * @param string $username
     * @param string $password
     * @return array
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    private static function http_auth($username, $password)
    {
        $results = array();
        if (($_SERVER['REMOTE_USER'] == $username) ||
            ($_SERVER['HTTP_REMOTE_USER'] == $username)) {
            $results['success']     = true;
            $results['type']        = 'http';
            $results['username']    = $username;
            $results['name']        = $username;
            $results['email']       = '';
        } else {
            $results['success'] = false;
            $results['error']   = 'HTTP auth login attempt failed';
        }

        return $results;
    } // http_auth

    /**
     * openid_auth
     * Authenticate user with OpenID
     *
     * @param string $username
     * @param string $password
     * @return array
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    private static function openid_auth($username, $password)
    {
        $results = array();
        // Username contains the openid url. We don't care about password here.
        $website = $username;
        if (strpos($website, 'http://') === 0 || strpos($website, 'https://') === 0) {
            $consumer = Openid::get_consumer();
            if ($consumer) {
                $auth_request = $consumer->begin($website);
                if ($auth_request) {
                    $sreg_request = Auth_OpenID_SRegRequest::build(
                        // Required
                        array('nickname'),
                        // Optional
                        array('fullname', 'email')
                    );
                    if ($sreg_request) {
                        $auth_request->addExtension($sreg_request);
                    }
                    $pape_request = new Auth_OpenID_PAPE_Request(Openid::get_policies());
                    if ($pape_request) {
                        $auth_request->addExtension($pape_request);
                    }

                    // Redirect the user to the OpenID server for authentication.
                    // Store the token for this authentication so we can verify the response.

                    // For OpenID 1, send a redirect.  For OpenID 2, use a Javascript
                    // form to send a POST request to the server.
                    if ($auth_request->shouldSendRedirect()) {
                        $redirect_url = $auth_request->redirectURL(AmpConfig::get('web_path'), Openid::get_return_url());
                        if (Auth_OpenID::isFailure($redirect_url)) {
                            $results['success'] = false;
                            $results['error']   = 'Could not redirect to server: ' . $redirect_url->message;
                        } else {
                            // Send redirect.
                            debug_event('auth', 'OpenID 1: redirecting to ' . $redirect_url, '5');
                            header("Location: " . $redirect_url);
                        }
                    } else {
                        // Generate form markup and render it.
                        $form_id   = 'openid_message';
                        $form_html = $auth_request->htmlMarkup(AmpConfig::get('web_path'), Openid::get_return_url(), false, array('id' => $form_id));

                        if (Auth_OpenID::isFailure($form_html)) {
                            $results['success'] = false;
                            $results['error']   = 'Could not render authentication form.';
                        } else {
                            debug_event('auth', 'OpenID 2: javascript redirection code to OpenID form.', '5');
                            // First step is a success, UI interaction required.
                            $results['success']     = false;
                            $results['ui_required'] = $form_html;
                        }
                    }
                } else {
                    debug_event('auth', $website . ' is not a valid OpenID.', '3');
                    $results['success'] = false;
                    $results['error']   = 'Not a valid OpenID.';
                }
            } else {
                debug_event('auth', 'Cannot initialize OpenID resources.', '3');
                $results['success'] = false;
                $results['error']   = 'Cannot initialize OpenID resources.';
            }
        } else {
            debug_event('auth', 'Skipped OpenID authentication: missing scheme in ' . $website . '.', '3');
            $results['success'] = false;
            $results['error']   = 'Missing scheme in OpenID.';
        }

        return $results;
    }

    /**
     * openid_auth_2
     * Authenticate user with OpenID, step 2
     * @return array
     */
    private static function openid_auth_2()
    {
        $results            = array();
        $results['type']    = 'openid';
        $consumer           = Openid::get_consumer();
        if ($consumer) {
            $response = $consumer->complete(Openid::get_return_url());

            if ($response->status == Auth_OpenID_CANCEL) {
                $results['success'] = false;
                $results['error']   = 'OpenID verification cancelled.';
            } else {
                if ($response->status == Auth_OpenID_FAILURE) {
                    $results['success'] = false;
                    $results['error']   = 'OpenID authentication failed: ' . $response->message;
                } else {
                    if ($response->status == Auth_OpenID_SUCCESS) {
                        // Extract the identity URL and Simple Registration data (if it was returned).
                        $sreg_resp    = Auth_OpenID_SRegResponse::fromSuccessResponse($response);
                        $sreg         = $sreg_resp->contents();

                        $results['website'] = $response->getDisplayIdentifier();
                        if (@$sreg['email']) {
                            $results['email'] = $sreg['email'];
                        }

                        if (@$sreg['nickname']) {
                            $results['username'] = $sreg['nickname'];
                        }

                        if (@$sreg['fullname']) {
                            $results['name'] = $sreg['fullname'];
                        }

                        $users = User::get_from_website($results['website']);
                        if (count($users) > 0) {
                            if (count($users) == 1) {
                                $user                = new User($users[0]);
                                $results['success']  = true;
                                $results['username'] = $user->username;
                            } else {
                                // Several users for the same website/openid? Allowed but stupid, try to get a match on username.
                                // Should we make website field unique?
                                foreach ($users as $id) {
                                    $user = new User($id);
                                    if ($user->username == $results['username']) {
                                        $results['success']  = true;
                                        $results['username'] = $user->username;
                                    }
                                }
                            }
                        } else {
                            // Don't return success if an user already exists for this username but don't have this openid identity as website
                            $user = User::get_from_username($results['username']);
                            if ($user->id) {
                                $results['success'] = false;
                                $results['error']   = 'No user associated to this OpenID and username already taken.';
                            } else {
                                $results['success'] = true;
                                $results['error']   = 'No user associated to this OpenID.';
                            }
                        }
                    }
                }
            }
        }

        return $results;
    }
}
