Plugin Directory

Changeset 2310981


Ignore:
Timestamp:
05/24/2020 07:30:32 AM (6 years ago)
Author:
scoop082110
Message:

New features for 1.1.

The main enhancement is 'guest' user. An external login service may be
configured to have a guest user, which is used in case when the email
address from the service does not correspond to a specific WordPress
user. Guest user must not have certain capabilities (e.g. 'edit_posts'),
and they cannot access the dashboard or update their own profile.

Other changes include:

  • Minor UI enhancement: 'Esc' closes modal windows.
  • Customizable info line after login buttons.

This corresponds to commit '10a3353f2ba0128e7ff1f38597f225e8dc416420'
in https://github.com/plai2010/xlogin/.

Location:
xlogin
Files:
2 added
12 edited

Legend:

Unmodified
Added
Removed
  • xlogin/trunk/html/admin/xsvcs.html

    r2265498 r2310981  
    4141                        No import from external profile.
    4242                    </span>
     43                </td>
     44            </tr>
     45            <tr>
     46                <td>Guest user:</td>
     47                <td>
     48                    <input type="text" v-model="svc.data.guest"
     49                        placeholder="Guest user for unknown aliases"
     50                    >
     51                    <button type="button"
     52                        @click="checkGuestUser()"
     53                        :disabled="guestUserNotSpecified"
     54                    >Check</button>
    4355                </td>
    4456            </tr>
  • xlogin/trunk/html/admin/xsvcs.js

    r2265498 r2310981  
    1717            xsvcs: []
    1818        },
     19        created() {
     20            window.addEventListener('keyup', e => {
     21                if (!this.svc)
     22                    return;
     23                if (e.key == 'Escape') {
     24                    this.svc = null;
     25                }
     26            });
     27        },
    1928        mounted() {
    2029            pl2010_XLoginApi.get('/xsvcs').done(resp => {
     
    2938        },
    3039        computed: {
     40            guestUserNotSpecified: function() {
     41                let xs = this.svc;
     42                if (!xs || !xs.data || !xs.data.guest)
     43                    return true;
     44                if (xs.data.guest.trim().length == 0)
     45                    return true;
     46                return null;
     47            },
    3148            incompleteSvcConfig: function() {
    3249                let xs = this.svc;
     
    6077        },
    6178        methods: {
     79            /**
     80             * Check if a guest user is acceptable.
     81             */
     82            checkGuestUser() {
     83                if (this.guestUserNotSpecified)
     84                    return;
     85                let guest = this.svc.data.guest.trim();
     86                this.svc.data.guest = guest;
     87                pl2010_XLoginApi.post('/admin', {
     88                    op: 'check-guest',
     89                    params: {
     90                        login: guest
     91                    }
     92                }).done(resp => {
     93                    if (this.guestUserNotSpecified)
     94                        return;
     95                    if (this.svc.data.guest != guest)
     96                        return;
     97                    let result = resp.result || {};
     98                    if (result.success) {
     99                        if (result.login)
     100                            this.svc.data.guest = result.login;
     101                        alert('Guest user acceptable.');
     102                    }
     103                    else {
     104                        alert('Guest user reject: ' + result.err_msg);
     105                    }
     106                });
     107            },
     108
    62109            /**
    63110             * Configure external login service.
  • xlogin/trunk/html/admin/xusers.js

    r2264830 r2310981  
    4646            },
    4747            xusers: []
     48        },
     49        created() {
     50            window.addEventListener('keyup', e => {
     51                if (!this.modal.add && !this.modal.upload)
     52                    return;
     53                if (e.key == 'Escape') {
     54                    this.modal.add = false;
     55                    this.modal.upload = false;
     56                }
     57            });
    4858        },
    4959        mounted() {
  • xlogin/trunk/includes/admin.php

    r2264830 r2310981  
    7575    // }}}
    7676    //------------------------------------------------------
     77    // Customization section. {{{
     78    //
     79    add_settings_section(
     80        'pl2010-xlogin-customize',
     81        __('Customization', 'pl2010'),
     82        function($args) {
     83            $admin = __DIR__."/../html/admin";
     84            echo file_get_contents("{$admin}/customize.html");
     85            ?>
     86            <script type="text/javascript">
     87            <?php
     88            echo file_get_contents("{$admin}/customize.js");
     89            ?>
     90            </script>
     91            <?php
     92        },
     93        'pl2010-xlogin'
     94    );
     95    // }}}
     96    //------------------------------------------------------
    7797} /*}}}*/);
    7898
  • xlogin/trunk/includes/api.php

    r2264830 r2310981  
    1515    $namespace = 'pl2010/xlogin/v1';
    1616
     17    //--------------------------------------------------------------
     18    // Retrieve customization configuration. {{{
     19    //
     20    register_rest_route($namespace, '/customize', [
     21        'methods' => 'GET',
     22        'callback' => function($req) use($xlogin) {
     23            $conf = $xlogin->getCustomization();
     24            return XLoginApi::checkError($conf) ?? [
     25                'data' => $conf,
     26            ];
     27        },
     28        'permission_callback' => 'PL2010\WordPress\XLoginApi::checkPermRead',
     29    ]);
     30    // }}}
     31    //--------------------------------------------------------------
     32    // Update customization configuration. {{{
     33    //
     34    register_rest_route($namespace, '/customize', [
     35        'methods' => 'POST',
     36        'callback' => function($req) use($xlogin) {
     37            $conf = $req->get_param('data');
     38            $conf = $xlogin->updateCustomization($conf);
     39            return XLoginApi::checkError($conf) ?? [
     40                'data' => $conf,
     41            ];
     42        },
     43        'args' => [
     44            'data' => [
     45                'required' => true,
     46                'validate_callback' => function($data) {
     47                    return is_array($data);
     48                },
     49            ],
     50        ],
     51        'permission_callback' => 'PL2010\WordPress\XLoginApi::checkPermUpdate',
     52    ]);
     53    // }}}
     54    //--------------------------------------------------------------
     55    // Miscellaneous admin API. {{{
     56    //
     57    register_rest_route($namespace, '/admin', [
     58        'methods' => 'POST',
     59        'callback' => function($req) use($xapi) {
     60            $op = $req->get_param('op');
     61            $params = $req->get_param('params');
     62            $result = $xapi->performAdminOp($op, $params);
     63            return XLoginApi::checkError($result) ?? [
     64                'result' => $result,
     65            ];
     66        },
     67        'args' => [
     68            'op' => [
     69                'required' => true,
     70                'validate_callback' => function($op) {
     71                    return is_string($op);
     72                },
     73            ],
     74            'params' => [
     75                'required' => false,
     76                'validate_callback' => function($params) {
     77                    return is_array($params);
     78                },
     79            ],
     80        ],
     81        'permission_callback' => 'PL2010\WordPress\XLoginApi::checkPermAdmin',
     82    ]);
     83    // }}}
    1784    //--------------------------------------------------------------
    1885    // Get list of external services. {{{
  • xlogin/trunk/includes/login.php

    r2264830 r2310981  
    8484        filemtime("{$pluginDir}css/login.css")
    8585    );
     86
     87    $customize = $xlogin->getCustomization();
    8688    ?>
    8789    <div class="pl2010-xlogin-launch">
     
    133135            <?php
    134136        }
     137        if ($customize['login_buttons_info'] ?? null) {
     138            ?><p class="description"><?php
     139            esc_html_e($customize['login_buttons_info']);
     140            ?></p><?php
     141        }
    135142        ?>
    136143    </div>
  • xlogin/trunk/init.php

    r2264830 r2310981  
    107107        return $user;
    108108
    109     if ($auth != '')
    110         return $xlogin->getAuthenticated($auth, $name, $clear=true) ?? $user;
     109    if ($xu = $xlogin->getAuthenticated($auth, $name, $clear=true, $guest)) {
     110        if ($guest) {
     111            // Guest is not allowed to access admin page, so replace
     112            // use site URL if login redirect looks like the admin
     113            // page so that user does not get an error page.
     114            add_filter('login_redirect', function($url) {
     115                if (strpos($url, admin_url()) == 0)
     116                    return site_url();
     117                return $url;
     118            });
     119        }
     120        return $xu;
     121    }
    111122    return $user;
    112123} /*}}}*/, 10, 3);
  • xlogin/trunk/lib/XLogin.php

    r2264830 r2310981  
    1818use WP_User;
    1919
    20 use Exception;
     20use Throwable;
    2121
    2222/**
     
    6363 * aliases' (email addresses). It is thus possible to allow multiple
    6464 * users to share the same WordPress account without shared knowledge
    65  * of the WordPress password.
     65 * of the WordPress password. A 'guest' user may be configured as
     66 * wildcard for no-match situation.
    6667 *
    6768 * For privacy consideration, external aliases are stored in the
     
    7879    /** @var string Facebook Graph API version to request. */
    7980    public static $FACEBOOK_GRAPH_API_VERS = 'v3.3';
     81
     82    /** @var array Capabilities that a guest user must not have. */
     83    public static $GUEST_USER_FORBIDDEN_CAPS = [
     84        // TODO: configurable via settings
     85        'edit_posts',           // c.f. anyone above 'subscriber'
     86        'delete_posts',         // c.f. 'contributor' or above
     87        'publish_posts',        // c.f. 'author' or above
     88        'edit_pages',           // c.f. 'editor' or above
     89        'manage_options',       // c.f. 'administrator' or above
     90    ];
     91
     92    /** @var array Capabilities to disable for guest user. */
     93    public static $GUEST_USER_DISABLED_CAPS = [
     94        // TODO: configurable via settings
     95        'read',                 // access to dashboard and profile
     96        'edit_user',            // profile update (via REST API)
     97    ];
    8098
    8199    /** @var array External login service instances by name. */
     
    452470     * @param string $name External auth user name if known.
    453471     * @param boolean $clear Clear external auth from session.
     472     * @param boolean &$guest Tell if authenticated user is a guest.
    454473     * @return WP_User|WP_Error|NULL Null if no external user.
    455474     */
    456     public function getAuthenticated($type, $name=null, $clear=false) /*{{{*/
     475    public function getAuthenticated(
     476        $type,
     477        $name=null,
     478        $clear=false,
     479        &$guest=false
     480    ) /*{{{*/
    457481    {
    458482        $this->logDebug("getAuthenticated(): type=$type name=$name");
     483        $guest = null;
    459484
    460485        if (!($xu = $this->flowAttrGet("$type-xuser")))
     
    492517            assert($xu['id'] == $user->ID);
    493518            $this->xu_cache[$user->ID] = $xu;
     519            $guest = $xu['guest'] ?? false;
    494520        }
    495521        return $user;
     
    576602        $uri = $this->url_base.$this->getCallbackPathRelative($cb, $type);
    577603        return $uri;
     604    } /*}}}*/
     605
     606    /**
     607     * Get configuration items for customization.
     608     * @return array Customization config options.
     609     */
     610    public function getCustomization() /*{{{*/
     611    {
     612        $options = $this->getOptions();
     613        return $options['customize'] ?? null;
    578614    } /*}}}*/
    579615
     
    856892            return null;
    857893
     894        // Get guest login name from ID.
     895        if (is_int($guest = $provider['guest'] ?? null)) {
     896            if ($guest = get_user_by('ID', $guest))
     897                $provider['guest'] = $guest->user_login;
     898            else
     899                unset($provider['guest']);
     900            $this->options['providers'][$type] = $provider;
     901        }
     902
     903        // Decrypt auth config.
    858904        if ($config = $provider['config'] ?? null) {
    859905            if (!is_array($config))
     
    10141060     * external user information, and is thus sensitive to WordPress version.
    10151061     *
     1062     * @param boolean &$guest Flag to indicate if imported user is guest.
    10161063     * @return boolean True if import is done.
    10171064     */
    1018     public function importXUser() /*{{{*/
     1065    public function importXUser(&$guest=false) /*{{{*/
    10191066    {
    10201067        global $current_user;
    10211068        global $userdata, $user_ID, $user_identity, $user_email;
    10221069
     1070        $guest = null;
    10231071        if (!($xu = $this->getXUserInfo($user_ID)))
    10241072            return false;
     
    10461094                = $user_email
    10471095                = $xu['email'];
     1096        $guest = $xu['guest'] ?? false;
     1097
     1098        // Tighten access control for guests.
     1099        if ($guest) {
     1100            // Disable capabilities for guest.
     1101            add_filter('user_has_cap', function($allcaps, $caps, $args) {
     1102                $disabled = static::$GUEST_USER_DISABLED_CAPS;
     1103                if (!$disabled)
     1104                    return $allcaps;
     1105                $wanted = $args[0];
     1106                if ($allcaps[$wanted] ?? true && in_array($wanted, $disabled))
     1107                    $allcaps[$wanted] = false;
     1108                return $allcaps;
     1109            }, 10, 3);
     1110        }
    10481111
    10491112        return true;
     
    11491212            $info = call_user_func($info);
    11501213        call_user_func($this->dbg_logger, $info);
     1214    } /*}}}*/
     1215
     1216    /**
     1217     * Check if a WP user is acceptable as guest via external login.
     1218     *
     1219     * A guest user is one authenticated by an external login service
     1220     * but with an alias not recognized, and should have minimal
     1221     * privilege. Generally want 'subscriber' level, but role is
     1222     * fully customizable in WP, so check for some capabilities.
     1223     *
     1224     * @param WP_User $user User to check.
     1225     * @param string &$emsg Error message if not acceptable.
     1226     * @return boolean True if user acceptable as guest.
     1227     */
     1228    public function isAcceptableGuest($user, &$emsg=null) /*{{{*/
     1229    {
     1230        $emsg = null;
     1231
     1232        if (!$user instanceof WP_User) {
     1233            $emsg = 'Invalid user.';
     1234            return false;
     1235        }
     1236
     1237        foreach (static::$GUEST_USER_FORBIDDEN_CAPS ?? [] as $forbid) {
     1238            if ($user->has_cap($forbid)) {
     1239                $emsg = 'User is too privileged to be guest login.';
     1240                return false;
     1241            }
     1242        }
     1243
     1244        return true;
    11511245    } /*}}}*/
    11521246
     
    12391333        if (!$user)
    12401334            $user = $this->resolveXUserByAlias('email', $email);
     1335
     1336        // Attempt guest user if no specific one matched.
     1337        $guest = null;
     1338        if (!$user && !empty($config['guest'])) {
     1339            if ($guest = get_user_by('login', $config['guest'])) {
     1340                if ($this->isAcceptableGuest($guest))
     1341                    $user = $guest;
     1342                else
     1343                    $guest = null;
     1344            }
     1345        }
     1346
    12411347        if (!$user) {
    12421348            $this->setError(
     
    12531359        $xu['id'] = $user->ID;
    12541360        $xu['xtype'] = $type;
     1361        if ($guest)
     1362            $xu['guest'] = true;
    12551363        if (!$this->flowAttrSet("$type-xuser", $xu))
    12561364            return null;
     
    15721680        ];
    15731681
     1682        // Customization config items.
     1683        if (is_array($input['customize']??null)) {
     1684            foreach ($input['customize'] as $key => $val) {
     1685                switch ($key) {
     1686                case 'login_buttons_info':
     1687                    try {
     1688                        $val = strval($val);
     1689                        $data['customize'][$key] = sanitize_text_field($val);
     1690                    }
     1691                    catch (Throwable $err) {
     1692                        $this->logDebug("invalid customization '$key' value");
     1693                    }
     1694                    break;
     1695                default:
     1696                    $this->logDebug("unknown customization '$key'");
     1697                    break;
     1698                }
     1699            }
     1700        }
     1701
    15741702        // Login/auth provider configuration.
    15751703        if (is_array($input['providers']??null)) {
     
    15881716                    if ($provider['override'] ?? false) {
    15891717                        $slot['override'] = true;
     1718                    }
     1719
     1720                    $guest = $provider['guest'] ?? null;
     1721                    if (is_string($guest) && $guest != '') {
     1722                        $guest = get_user_by('login', $guest);
     1723                        if ($guest && $this->isAcceptableGuest($guest))
     1724                            $slot['guest'] = $guest->ID;
    15901725                    }
    15911726
     
    16391774
    16401775        return true;
     1776    } /*}}}*/
     1777
     1778    /**
     1779     * Update customization configuration items.
     1780     * @param array $data Config data.
     1781     * @return array|WP_Error Updated config data or error.
     1782     */
     1783    public function updateCustomization($data) /*{{{*/
     1784    {
     1785        $options = $this->getOptions();
     1786        $options['customize'] = $data;
     1787        $options = $this->sanitizeOptions($options);
     1788
     1789        if (!update_option($this->getOptionsName(), $options))
     1790            return new WP_Error('server_error', 'Failed to save option.');
     1791
     1792        // Reload options.
     1793        $this->getOptions($cache=false);
     1794        return $this->getCustomization();
    16411795    } /*}}}*/
    16421796
  • xlogin/trunk/lib/XLoginApi.php

    r2264830 r2310981  
    4343            'error_description' => $result->get_error_message(),
    4444        ];
     45    } /*}}}*/
     46
     47    /**
     48     * REST callback to check permission for general admin operations.
     49     * @return boolean
     50     */
     51    public static function checkPermAdmin() /*{{{*/
     52    {
     53        return current_user_can('manage_options');
    4554    } /*}}}*/
    4655
     
    231240
    232241    /**
     242     * Perform a miscellaneous admin operation.
     243     * @param string $op Operation to perform.
     244     * @param array $params Parameters for the operation.
     245     * @return array|WP_Error Operation result, or WP_Error.
     246     */
     247    public function performAdminOp($op, $params) /*{{{*/
     248    {
     249        $result = [];
     250        $SUCCESS =& $result['success'];
     251        $ERR_MSG =& $result['err_msg'];
     252
     253        switch ($op) {
     254        case 'check-guest':
     255            $guestLogin = $params['login'] ?? null;
     256            if ($guestLogin == '')
     257                return new WP_Error('input-invalid', 'Missing guest login.');
     258            $guestUser = static::lookupWpUser($guestLogin);
     259            if (!$guestUser) {
     260                $SUCCESS = false;
     261                $ERR_MSG = 'Invalid guest login.';
     262                break;
     263            }
     264            if ($SUCCESS = $this->xlogin->isAcceptableGuest($guestUser, $emsg))
     265                $result['login'] = $guestUser->user_login;
     266            else
     267                $ERR_MSG = $emsg;
     268            break;
     269        default:
     270            return new WP_Error(
     271                'input-invalid',
     272                "Unknown admin operation '$op'."
     273            );
     274        }
     275        return $result;
     276    } /*}}}*/
     277
     278    /**
    233279     * Register an external login.
    234280     * @param string $alias External alias, e.g. 'email:jdoe@example.com'.
  • xlogin/trunk/readme.txt

    r2265492 r2310981  
    11=== XLogin ===
    22Contributors: scoop082110
    3 Donate link: https://www.paypal.me/scoop082110
    43Tags: login, oauth, google, yahoo, facebook
    54Requires at least: 5.3
    6 Tested up to: 5.3
    7 Stable tag: 1.0
     5Tested up to: 5.4
     6Stable tag: 1.1
    87Requires PHP: 7.0
    98License: GPLv2 or later
     
    5958WordPress session when he logs in with Facebook.
    6059
     60Finally XLogin has the notion of 'guest' user. When an email address
     61provided by an external service does not correspond to a specific
     62WordPress user, XLogin will proceed with a guest WordPress user
     63configured for the service. There are plugins that control access to
     64content based on user roles; a site can combine such with XLogin to
     65control what is visible to guests. Note that guest WordPress users
     66should have minimal privileges. XLogin has safeguards to ensure, for
     67example, that a guest cannot edit posts. It also filters out guest's
     68permission to access the dashboard or to update their own profile.
     69
    6170== Installation ==
    6271
     
    94103= Does this work with WordPress version X? =
    95104
    96 This plugin is developed on WordPress 5.3. It has not been tried on any
    97 other version.
     105This plugin was originally developed on WordPress 5.3. It works on 5.3
     106and 5.4, but has not been tried on any other version.
    98107
    99108= Does this work with PHP 5.x? =
     
    177186== Changelog ==
    178187
     188= 1.1 =
     189* Custom message to display with external login buttons.
     190* Guest user for unmatched email address.
     191
    179192= 1.0 =
    180193* First version published.
Note: See TracChangeset for help on using the changeset viewer.