├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── standard_bug.md ├── PULL_REQUEST_TEMPLATE │ └── standard_pr.md ├── check-php-syntax.php └── workflows │ └── Syntax-Check.yml ├── .gitignore ├── DCO.txt ├── DatabaseProvider ├── MySQLi.php ├── PDO.php └── base.php ├── ForumAuthManager.php ├── ForumProvider ├── base.php ├── elk1.0.php ├── elk1.1.php ├── smf2.0.php └── smf2.1.php ├── ForumSsoProvider.php ├── LICENSE.md ├── README.md ├── composer.json └── extension.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force our line endings to be LF, even for Windows 2 | * text eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/standard_bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Standard SMF Bug report 4 | 5 | --- 6 | 7 | #### Description 8 | --Description Here-- 9 | 10 | ### Steps to reproduce 11 | 1. 12 | 2. 13 | 14 | ### Environment (complete as necessary) 15 | - Version/Git revision: 16 | - Database Type: 17 | - Database Version: 18 | - PHP Version: 19 | 20 | 21 | ### Additional information/references -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/standard_pr.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull Request 3 | about: Standard SMF Pull Request 4 | 5 | --- 6 | 7 | #### Description 8 | --PR Summary here-- 9 | 10 | ### Issues References (Fixes|Related|Closes) 11 | 1. 12 | 2. 13 | -------------------------------------------------------------------------------- /.github/check-php-syntax.php: -------------------------------------------------------------------------------- 1 | $fileInfo) 31 | { 32 | // Only check PHP 33 | if ($fileInfo->getExtension() !== 'php') 34 | continue; 35 | 36 | foreach ($ignoreFiles as $if) 37 | if (preg_match('~' . $if . '~i', $currentFile)) 38 | continue 2; 39 | 40 | # Always check against the base. 41 | $result = trim(shell_exec('php -l ' . $currentFile)); 42 | 43 | if (!preg_match('~No syntax errors detected in ' . $currentFile . '~', $result)) 44 | { 45 | $foundBad = true; 46 | fwrite(STDERR, 'PHP via $PATH: ' . $result . "\n"); 47 | continue; 48 | } 49 | 50 | // We have additional binaries we want to test against? 51 | foreach ($addditionalPHPBinaries as $binary) 52 | { 53 | $binary = trim($binary); 54 | $result = trim(shell_exec($binary . ' -l ' . $currentFile)); 55 | 56 | if (!preg_match('~No syntax errors detected in ' . $currentFile . '~', $result)) 57 | { 58 | $foundBad = true; 59 | fwrite(STDERR, 'PHP via ' . $binary . ': ' . $result . "\n"); 60 | continue 2; 61 | } 62 | } 63 | } 64 | 65 | if (!empty($foundBad)) 66 | exit(1); -------------------------------------------------------------------------------- /.github/workflows/Syntax-Check.yml: -------------------------------------------------------------------------------- 1 | name: PHP Syntax Check 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | syntax-checker: 7 | runs-on: ${{ matrix.operating-system }} 8 | strategy: 9 | matrix: 10 | operating-system: [ ubuntu-latest ] 11 | php: [ '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2' ] 12 | name: PHP ${{ matrix.php }} Syntax Check 13 | steps: 14 | - uses: actions/checkout@master 15 | with: 16 | submodules: true 17 | - name: Setup PHP 18 | id: SetupPHP 19 | uses: nanasess/setup-php@master 20 | with: 21 | php-version: ${{ matrix.php }} 22 | - run: php ./.github/check-php-syntax.php ./ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | -------------------------------------------------------------------------------- /DCO.txt: -------------------------------------------------------------------------------- 1 | Developer's Certificate of Origin 1.1 2 | 3 | By making a contribution to this project, I certify that: 4 | 5 | (a) The contribution was created in whole or in part by me and I 6 | have the right to submit it under the open source license 7 | indicated in the file; or 8 | 9 | (b) The contribution is based upon previous work that, to the best 10 | of my knowledge, is covered under an appropriate open source 11 | license and I have the right under that license to submit that 12 | work with modifications, whether created in whole or in part 13 | by me, under the same open source license (unless I am 14 | permitted to submit under a different license), as indicated 15 | in the file; or 16 | 17 | (c) The contribution was provided directly to me by some other 18 | person who certified (a), (b) or (c) and I have not modified 19 | it. 20 | 21 | (d) I understand and agree that this project and the contribution 22 | are public and that a record of the contribution (including all 23 | personal information I submit with it, including my sign-off) is 24 | maintained indefinitely and may be redistributed consistent with 25 | this project or the open source license(s) involved. -------------------------------------------------------------------------------- /DatabaseProvider/MySQLi.php: -------------------------------------------------------------------------------- 1 | report_mode = MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT; 38 | 39 | $this->db = @new mysqli($db_server, $db_user, $db_password, $db_name); 40 | 41 | if (!empty($this->db->connect_error)) 42 | { 43 | $this->DatabaseError($this->db->connect_error); 44 | return false; 45 | } 46 | 47 | $this->loaded = true; 48 | } catch (\Exception $e) { 49 | $this->DatabaseError($e); 50 | return false; 51 | } 52 | 53 | return true; 54 | } 55 | 56 | /** 57 | * Wrapper for MySQLi Query 58 | * 59 | * @param string $query The query we will perform against the database. 60 | * @return resource|false Database resource 61 | */ 62 | public function query(string $query) 63 | { 64 | try { 65 | $request = $this->db->query($query); 66 | } catch (\mysqli_sql_exception $e) { 67 | $this->DatabaseError($e); 68 | return false; 69 | } 70 | 71 | return $request; 72 | } 73 | 74 | /** 75 | * Wrapper for MySQLi fetch_assoc. 76 | * 77 | * @param resource $request Database resource 78 | * @return array|bool The data returned. 79 | */ 80 | public function fetch_assoc($request) 81 | { 82 | if (empty($request)) 83 | return false; 84 | 85 | try { 86 | $row = $request->fetch_assoc(); 87 | } catch (\mysqli_sql_exception $e) { 88 | $this->FSDBError($e); 89 | return false; 90 | } 91 | 92 | return $row; 93 | } 94 | 95 | /** 96 | * Wrapper for MySQLi fetch_row. 97 | * 98 | * @param resource $request Database resource 99 | * @return array|bool The data returned. 100 | */ 101 | public function fetch_row($request) 102 | { 103 | if (empty($request)) 104 | return false; 105 | 106 | try { 107 | $row = $request->fetch(); 108 | } catch (\mysqli_sql_exception $e) { 109 | $this->FSDBError($e); 110 | return false; 111 | } 112 | 113 | return $row; 114 | } 115 | 116 | /** 117 | * Wrapper for MySQLi num_rows. 118 | * 119 | * @param resource $request Database resource 120 | * @return int|bool The number of rows, false if an error occured. 121 | */ 122 | public function fetch_num_rows($request) 123 | { 124 | if (empty($request)) 125 | return 0; 126 | 127 | try { 128 | return $request->num_rows; 129 | } catch (\mysqli_sql_exception $e) { 130 | $this->FSDBError($e); 131 | return 0; 132 | } 133 | } 134 | 135 | /** 136 | * Wrapper for MySQLi free_result. 137 | * 138 | * @param resource $request Database resource 139 | * @return bool False if request contained nothing, otehrwise we assume it worked. 140 | */ 141 | public function free(&$request): bool 142 | { 143 | if (empty($request)) 144 | return false; 145 | 146 | try { 147 | $request->free_result(); 148 | } catch (\mysqli_sql_exception $e) { 149 | $this->FSDBError($e); 150 | } 151 | 152 | return true; 153 | } 154 | 155 | /** 156 | * Wrapper for MySQLi real_escape_string. 157 | * 158 | * @param string $string The string we are escaping 159 | * @return string The string escaped and ready for usage in the database. 160 | */ 161 | public function quote(string $string): string 162 | { 163 | if (empty($string)) 164 | return false; 165 | 166 | try { 167 | $escaped = $this->db->real_escape_string($string); 168 | } catch (\mysqli_sql_exception $e) { 169 | $this->FSDBError($e); 170 | 171 | // better than nothing? 172 | return (string) addslashes($string); 173 | } 174 | 175 | return (string) $escaped; 176 | } 177 | 178 | } -------------------------------------------------------------------------------- /DatabaseProvider/PDO.php: -------------------------------------------------------------------------------- 1 | db_type === 'postgresql' ? 'pgsql' : 'mysql'; 37 | 38 | $this->db = new PDO( 39 | $type . ':host=' . $db_server . ';dbname=' . $db_name, 40 | $db_user, 41 | $db_user 42 | ); 43 | 44 | if (!empty($this->db->connect_error)) 45 | { 46 | throw new ErrorException($this->db->connect_error); 47 | return false; 48 | } 49 | 50 | $this->loaded = true; 51 | } catch (\Exception $e) { 52 | $this->DatabaseError($e); 53 | return false; 54 | } 55 | 56 | return true; 57 | } 58 | 59 | /** 60 | * Wrapper for PDO Query 61 | * 62 | * @param string $query The query we will perform against the database. 63 | * @return resource|bool Database resource 64 | */ 65 | public function query(string $request) 66 | { 67 | try { 68 | $statement = $this->db->prepare($query); 69 | $request = $statement->execute(); 70 | } catch (\PDOException $e) { 71 | $this->DatabaseError($e); 72 | return false; 73 | } 74 | 75 | return $request; 76 | } 77 | 78 | /** 79 | * Wrapper for PDO fetch using assoc. 80 | * 81 | * @param resource $request Database resource 82 | * @return array|bool The data returned. 83 | */ 84 | public function fetch_assoc($request) 85 | { 86 | if (empty($request)) 87 | return false; 88 | 89 | try { 90 | $row = $request->fetch(PDO::FETCH_ASSOC); 91 | } catch (\PDOException $e) { 92 | $this->FSDBError($e); 93 | return false; 94 | } 95 | 96 | return $row; 97 | } 98 | 99 | /** 100 | * Wrapper for PDO fetch using row. 101 | * 102 | * @param resource $request Database resource 103 | * @return array|bool The data returned. 104 | */ 105 | public function fetch_row($request) 106 | { 107 | if (empty($request)) 108 | return false; 109 | 110 | try { 111 | $row = $request->fetch(PDO::FETCH_NUM); 112 | } catch (\PDOException $e) { 113 | $this->FSDBError($e); 114 | return false; 115 | } 116 | 117 | return $row; 118 | } 119 | 120 | /** 121 | * Wrapper for PDO to fetch the number of rows. 122 | * 123 | * @param resource $request Database resource 124 | * @return int|bool The number of rows, false if an error occured. 125 | */ 126 | public function fetch_num_rows($request) 127 | { 128 | if (empty($request)) 129 | return 0; 130 | 131 | try { 132 | /* 133 | It should be noted that rowCount may not work on all databases. 134 | In MySQL at least this works 135 | References: https://www.php.net/manual/en/pdostatement.rowcount.php#example-1096 136 | References: https://stackoverflow.com/questions/11305230/alternative-for-mysql-num-rows-using-pdo#comment-32016656 137 | */ 138 | if ($this->db_type === 'mysql') 139 | return $request->rowCount(); 140 | // Performance could be a issue here.. 141 | else 142 | { 143 | $allRows = $request->fetchAll(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_COLUMN); 144 | $rowCount = count($allRows); 145 | unset($allRows); 146 | 147 | // Reset the counter and allow loops to work again. 148 | $request->closeCursor(); 149 | $request->execute(); 150 | 151 | return $rowCount; 152 | } 153 | } catch (\PDOException $e) { 154 | $this->FSDBError($e); 155 | return 0; 156 | } 157 | } 158 | 159 | /** 160 | * Wrapper for PDO closeCursor. 161 | * 162 | * @param resource $request Database resource 163 | * @return bool False if request contained nothing, otehrwise we assume it worked. 164 | */ 165 | public function free(&$request): bool 166 | { 167 | if (empty($request)) 168 | return false; 169 | 170 | try { 171 | $request->closeCursor(); 172 | } catch (\PDOException $e) { 173 | $this->FSDBError($e); 174 | } 175 | 176 | return true; 177 | } 178 | 179 | /** 180 | * Wrapper for PDO quote. 181 | * 182 | * @param string $string The string we are escaping 183 | * @return string The string escaped and ready for usage in the database. 184 | */ 185 | public function quote(string $string): string 186 | { 187 | if (empty($string)) 188 | return false; 189 | 190 | try { 191 | $escaped = $this->db->quote($string); 192 | } catch (\PDOException $e) { 193 | $this->FSDBError($e); 194 | 195 | // better than nothing? 196 | return (string) addslashes($string); 197 | } 198 | 199 | return (string) $escaped; 200 | } 201 | } -------------------------------------------------------------------------------- /DatabaseProvider/base.php: -------------------------------------------------------------------------------- 1 | MWlogger = &$MWlogger; 40 | $this->db_type = $db_type === 'postgresql' ? 'postgresql' : 'mysql'; 41 | } 42 | 43 | /** 44 | * Checks if we have loaded and connected to a database session for our forum software. 45 | * 46 | * @return bool True if connected, otherwise false. 47 | */ 48 | public function isLoaded(): bool 49 | { 50 | return $this->loaded; 51 | } 52 | 53 | /** 54 | * The database type. 55 | * 56 | * @return string The database type. 57 | */ 58 | public function getDbType(): string 59 | { 60 | return $this->db_type; 61 | } 62 | 63 | /** 64 | * Database error wrapper. Sends errors to the MediaWiki logger. 65 | * 66 | * @param Exception|string $error The error from the database, typically a exception object. 67 | * @return void No return is generated. 68 | */ 69 | protected function DatabaseError(object $error): void 70 | { 71 | if (is_object($error)) 72 | $this->MWlogger->debug( 73 | 'Database Error ({FSDBCODE}): {FSDBERROR}', 74 | array( 75 | 'FSDBCODE' => $error->getCode(), 76 | 'FSDBERROR' => $error->getMessage() 77 | )); 78 | else 79 | $this->MWlogger->debug($error); 80 | } 81 | } -------------------------------------------------------------------------------- /ForumAuthManager.php: -------------------------------------------------------------------------------- 1 | getDBLoadBalancerFactory(); 34 | $userOptionsLookup = \MediaWiki\MediaWikiServices::getInstance()->getUserOptionsLookup(); 35 | 36 | parent::__construct( $loadBalancer, $userOptionsLookup, $params ); 37 | } 38 | 39 | /** 40 | * @deprecated since 1.37. For extension-defined authentication providers 41 | * that were using this method to trigger other work, please override 42 | * AbstractAuthenticationProvider::postInitSetup instead. If your extension 43 | * was using this to explicitly change the Config of an existing 44 | * AuthenticationProvider object, please file a report on phabricator - 45 | * there is no non-deprecated way to do this anymore. 46 | * @param Config $config 47 | */ 48 | public function setConfig( \Config $config ) 49 | { 50 | parent::setConfig( $config ); 51 | } 52 | 53 | /** 54 | * Get password reset data, if any 55 | * 56 | * @stable to override 57 | * @param string $username 58 | * @param \stdClass|null $data 59 | * @return \stdClass|null { 'hard' => bool, 'msg' => Message } 60 | */ 61 | protected function getPasswordResetData( /*string */ $username, $data ): bool 62 | { 63 | return false; 64 | } 65 | 66 | /** 67 | * @param string $action 68 | * @param array $options 69 | * 70 | * @return array 71 | */ 72 | public function getAuthenticationRequests( $action, array $options ): array 73 | { 74 | return []; 75 | } 76 | 77 | /* 78 | * This is implanted just to disable password changes. 79 | * Return StatusValue::newGood( 'ignored' ) if you don't support this 80 | * AuthenticationRequest type. 81 | * 82 | * @param AuthenticationRequest $req 83 | * @param bool $checkData If false, $req hasn't been loaded from the 84 | * submission so checks on user-submitted fields should be skipped. 85 | * $req->username is considered user-submitted for this purpose, even 86 | * if it cannot be changed via $req->loadFromSubmission. 87 | * @return StatusValue 88 | */ 89 | public function providerAllowsAuthenticationDataChange( 90 | \MediaWiki\Auth\AuthenticationRequest $req, /*bool*/ $checkData = true 91 | ) 92 | { 93 | $rest = \StatusValue::newGood(); 94 | $rest->setOK(false); 95 | return $rest; 96 | } 97 | 98 | /* 99 | * This one disables any other properties we need to block 100 | * @see AuthManager::allowsPropertyChange() 101 | * @param string $property 102 | * @return bool 103 | */ 104 | public function providerAllowsPropertyChange( /*string*/ $property ): bool 105 | { 106 | if (in_array($property, array( 107 | 'realname', 108 | 'emailaddress' 109 | ))) 110 | return false; 111 | return true; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /ForumProvider/base.php: -------------------------------------------------------------------------------- 1 | MWlogger = &$MWlogger; 41 | $this->db = &$db; 42 | $this->ForumSettings = &$ForumSettings; 43 | } 44 | 45 | /** 46 | * Validate that the configuration file exists and is readable. 47 | * 48 | * @param string $basepath The base path to the forum configuraiton file. The forum handler will load the appropriate configuraiton file. 49 | * @return bool True if its valid, false otherwise. 50 | */ 51 | public function configurationFileIsValid(string $basepath): bool 52 | { 53 | return false; 54 | } 55 | 56 | /** 57 | * Reads the configuration file and loads the data into $ForumSetings for use later. 58 | * 59 | * @param string $basepath The base path to the forum configuraiton file. The forum handler will load the appropriate configuraiton file. 60 | * @return void|bool Null if we loaded up data, false if it failed. 61 | */ 62 | public function readConfigurationFile(string $basepath) 63 | { 64 | return false; 65 | } 66 | 67 | /** 68 | * Determines if the cookie for the forum software exists. 69 | * 70 | * @return bool True if the forum software cookie exists, false otherwise. 71 | */ 72 | public function cookieExists(): bool 73 | { 74 | return false; 75 | } 76 | 77 | /** 78 | * Decodes the forum software cookie returning the id and password. 79 | * 80 | * @return array The ID and password. 81 | */ 82 | public function decodeCookie(): array 83 | { 84 | return ['id' => 0, 'password' => '']; 85 | } 86 | 87 | /* 88 | * Figure out the URL that we need to send the user to in order to perform the requested action. 89 | * The forum software should process the action and then return to MediaWiki. 90 | * 91 | * @param string $action The action that MediaWiki took under its special page. 92 | * @param string $wiki_url The url we should return to once we have completed the action from the forum. 93 | * @param bool $do_return If we should return to the wiki or not. 94 | * @return string $forum_url The url to the forum we need to go do. 95 | */ 96 | public function getRedirectURL(string $action, string $wiki_url, bool $do_return = false): string 97 | { 98 | // The base. 99 | $forum_url = 100 | $this->ForumSettings['boardurl'] 101 | . '/index.php?action=' . $action; 102 | 103 | return $forum_url; 104 | } 105 | 106 | /* 107 | * Validate that the cookie has a valid password for the user. This should ensure 108 | * that forged cookies or if a password has changed that the cookie rejects the login. 109 | * 110 | * @param array $user The user data. 111 | * @param array $cookie The cookie data. 112 | * @return bool True if the cookie is valid, false otherwise. 113 | */ 114 | public function cookiePasswordIsValid(array $user, array $cookie): bool 115 | { 116 | return false; 117 | } 118 | 119 | /* 120 | * Retrieves the member ID as defined by the forum software. Typically this is the same as the ID provided 121 | * by the cookie but may differ depending on forum softwares. 122 | * 123 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 124 | depend on the data from getForumMember having existed due to caching. 125 | * @return array All data from the forum database. 126 | */ 127 | public function getForumMember(int $id_member): array 128 | { 129 | return []; 130 | } 131 | 132 | /* 133 | * Retrieves the member ID as defined by the forum software. Typically this is the same as the ID provided 134 | * by the cookie but may differ depending on forum softwares. 135 | * 136 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 137 | depend on the data from getForumMember having existed due to caching. 138 | * @return array All data from the forum database. 139 | */ 140 | public function getMemberID(array $member): int 141 | { 142 | return 0; 143 | } 144 | 145 | /* 146 | * Retrieves the member display name as defined by the forum software. 147 | * 148 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 149 | depend on the data from getForumMember having existed due to caching. 150 | * @return string The member display name. 151 | */ 152 | public function getMemberName(array $member): string 153 | { 154 | return 'Guest'; 155 | } 156 | 157 | /* 158 | * Retrieves the member groups as defined by the forum software. 159 | * 160 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 161 | depend on the data from getForumMember having existed due to caching. 162 | * @return array An array of intergers of the forum group ids this member is apart of. 163 | */ 164 | public function getMemberGroups(array $member): array 165 | { 166 | return []; 167 | } 168 | 169 | /* 170 | * Retrieves the member email address as defined by the forum software. 171 | * 172 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 173 | depend on the data from getForumMember having existed due to caching. 174 | * @return string The member email address. 175 | */ 176 | public function getMemberEmailAddress(array $member): string 177 | { 178 | return 'guest@example.com'; 179 | } 180 | 181 | /* 182 | * Retrieves the member login name as defined by the forum software. Typically this is the same as the ID provided 183 | * by the cookie but may differ depending on forum softwares. 184 | * 185 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 186 | depend on the data from getForumMember having existed due to caching. 187 | * @return string The member login name. 188 | */ 189 | public function getMemberRealName(array $member): string 190 | { 191 | return 'Guest'; 192 | } 193 | 194 | /* 195 | * Check if the member is banned in the forum software. If so let the SSO know to prevent them from 196 | * logging into the wiki. 197 | * 198 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 199 | depend on the data from getForumMember having existed due to caching. 200 | * @return bool True if banned, false otherwise. 201 | */ 202 | public function checkBans(array $member): bool 203 | { 204 | return false; 205 | } 206 | } -------------------------------------------------------------------------------- /ForumProvider/elk1.0.php: -------------------------------------------------------------------------------- 1 | ForumSettings['cookiename']], true); 32 | } 33 | 34 | /* 35 | * Figure out the URL that we need to send the user to in order to perform the requested action. 36 | * The forum software should process the action and then return to MediaWiki. 37 | * 38 | * @param string $action The action that MediaWiki took under its special page. 39 | * @param string $wiki_url The url we should return to once we have completed the action from the forum. 40 | * @param bool $do_return If we should return to the wiki or not. 41 | * @return string $forum_url The url to the forum we need to go do. 42 | */ 43 | public function getRedirectURL(string $action, string $wiki_url, bool $do_return = false): string 44 | { 45 | $forum_action = $this->validRedirectActions[$action]; 46 | 47 | $forum_url = 48 | $this->ForumSettings['boardurl'] 49 | . '/index.php?action=' . $forum_action; 50 | 51 | return $forum_url; 52 | } 53 | 54 | /* 55 | * Validate that the cookie has a valid password for the user. This should ensure 56 | * that forged cookies or if a password has changed that the cookie rejects the login. 57 | * 58 | * @param array $user The user data. 59 | * @param array $cookie The cookie data. 60 | * @return bool True if the cookie is valid, false otherwise. 61 | */ 62 | public function cookiePasswordIsValid(array $user, array $cookie): bool 63 | { 64 | return $cookie['password'] === hash('sha256', $user['passwd'] . $user['password_salt']); 65 | } 66 | 67 | /* 68 | * Check if the member is banned in the forum software. If so let the SSO know to prevent them from 69 | * logging into the wiki. 70 | * 71 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 72 | depend on the data from getForumMember having existed due to caching. 73 | * @return bool True if banned, false otherwise. 74 | */ 75 | public function checkBans(array $member): bool 76 | { 77 | $banned = $this->__check_basic_ban((int) $member['id_member']); 78 | return false; 79 | } 80 | 81 | /* 82 | * Elk does not need to use the legacy conversion for the MediaWiki SMF_Auth.php 83 | * 84 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 85 | * depend on the data from getForumMember having existed due to caching. 86 | * @param object $wikiuser The wiki user object provided for updating options. Note while getOption/setOption 87 | * are deprecated, we still use them here as this legacy option should not be required in the future. 88 | * @return bool True if we had to convert the setting and update the user, false otherwise. 89 | */ 90 | public function legacyUpdateWikiUser(array $member, array $wikiUser): bool 91 | { 92 | return false; 93 | } 94 | } -------------------------------------------------------------------------------- /ForumProvider/elk1.1.php: -------------------------------------------------------------------------------- 1 | 'register', 44 | 'userlogin' => 'login', 45 | 'userlogout' => 'logout' 46 | ]; 47 | 48 | protected $wikiMemberOptions = null; 49 | 50 | /** 51 | * Validate that the configuration file exists and is readable. 52 | * 53 | * @param string $basepath The base path to the forum configuraiton file. The forum handler will load the appropriate configuraiton file. 54 | * @return bool True if its valid, false otherwise. 55 | */ 56 | public function configurationFileIsValid(string $basepath): bool 57 | { 58 | return !empty($basepath) && is_readable($basepath . '/Settings.php'); 59 | } 60 | 61 | /** 62 | * Reads the configuration file and loads the data into $ForumSetings for use later.. 63 | * 64 | * @param string $basepath The base path to the forum configuraiton file. The forum handler will load the appropriate configuraiton file. 65 | * @return void|bool Null if we loaded up data, false if it failed. 66 | */ 67 | public function readConfigurationFile(string $basepath) 68 | { 69 | foreach ($this->settingsFileVariables as $key) 70 | global $$key; 71 | 72 | include ($basepath . '/Settings.php'); 73 | 74 | // Put these away for later. 75 | foreach ($this->settingsFileVariables as $key) 76 | $this->ForumSettings[$key] = !empty($GLOBALS[$key]) ? $GLOBALS[$key] : null; 77 | } 78 | 79 | /* 80 | * A compatiblity layer for Auth_SMF.php extension settings. 81 | * 82 | * @return void No return is expected. 83 | */ 84 | public function compatLegacy(): void 85 | { 86 | global $wgSMFPath, $wgSMFDenyGroupID, $wgSMFGroupID, $wgSMFAdminGroupID, $wgSMFSpecialGroups, $wgFSPNameStyle, $wgFSPEnableBanCheck; 87 | 88 | $this->MWlogger->debug('Detected SMF_Auth settings, loading compatibilty layer.'); 89 | 90 | $this->ForumSettings['path'] = isset($wgSMFPath) ? $wgSMFPath : '../forum'; 91 | 92 | // We only need to load settings that where in Auth_SMF, loadFSSettings handles the standard settings. 93 | foreach (array( 94 | 'LoginDeniedGroups' => 'wgSMFDenyGroupID', 95 | 'LoginAllowedGroups' => 'wgSMFGroupID', 96 | 'AdminGroups' => 'wgSMFAdminGroupID', 97 | 'SuperAdminGroups' => 'wgSMFAdminGroupID', 98 | 'InterfaceGroups' => 'wgSMFAdminGroupID', 99 | 'SpecialGroups' => 'wgSMFSpecialGroups', 100 | ) as $key => $value) 101 | $this->ForumSettings[$key] = !empty($$value) ? $$value : $this->ForumSettings[$key]; 102 | 103 | // Set the login style to SMF 104 | if (empty($this->ForumSettings['NameStyle'])) 105 | $this->ForumSettings['NameStyle'] = 'smf'; 106 | 107 | // The old Auth_SMF plugin did ban checks, enable it unless specified. 108 | if (!isset($this->ForumSettings['EnableBanCheck'])) 109 | $this->ForumSettings['EnableBanCheck'] = true; 110 | } 111 | 112 | /** 113 | * Determines if the cookie for the forum software exists. 114 | * 115 | * @return bool True if the forum software cookie exists, false otherwise. 116 | */ 117 | public function cookieExists(): bool 118 | { 119 | return !empty($_COOKIE[$this->ForumSettings['cookiename']]); 120 | } 121 | 122 | /** 123 | * Decodes the forum software cookie returning the id and password. 124 | * 125 | * @return array The ID and password. 126 | */ 127 | public function decodeCookie(): array 128 | { 129 | return (array) unserialize($_COOKIE[$this->ForumSettings['cookiename']]); 130 | } 131 | 132 | /* 133 | * Figure out the URL that we need to send the user to in order to perform the requested action. 134 | * The forum software should process the action and then return to MediaWiki. 135 | * 136 | * @param string $action The action that MediaWiki took under its special page. 137 | * @param string $wiki_url The url we should return to once we have completed the action from the forum. 138 | * @param bool $do_return If we should return to the wiki or not. 139 | * @return string $forum_url The url to the forum we need to go do. 140 | */ 141 | public function getRedirectURL(string $action, string $wiki_url, bool $do_return = false): string 142 | { 143 | $forum_action = $this->validRedirectActions[$action]; 144 | 145 | $forum_url = 146 | $this->ForumSettings['boardurl'] 147 | . '/index.php?action=' . $forum_action 148 | . ';return_hash=' . hash_hmac('sha1', $wiki_url, $this->ForumSettings['image_proxy_secret']) 149 | . ';return_to=' . urlencode($wiki_url); 150 | 151 | return (string) $forum_url; 152 | } 153 | 154 | /* 155 | * Validate that the cookie has a valid password for the user. This should ensure 156 | * that forged cookies or if a password has changed that the cookie rejects the login. 157 | * 158 | * @param array $user The user data. 159 | * @param array $cookie The cookie data. 160 | * @return bool True if the cookie is valid, false otherwise. 161 | */ 162 | public function cookiePasswordIsValid(array $user, array $cookie): bool 163 | { 164 | // 2.0.16 added a hash secret for cookies, preventing forgeries of the cookie, use it if enabled. 165 | if (!empty($this->ForumSettings['auth_secret']) && empty($this->ForumSettings['cookie_no_auth_secret'])) 166 | return $cookie['password'] === hash_hmac('sha1', sha1($user['passwd'] . $user['password_salt']), $this->ForumSettings['auth_secret']); 167 | else 168 | return $cookie['password'] === sha1($user['passwd'] . $user['password_salt']); 169 | } 170 | 171 | /* 172 | * Retrieves the forum member data as defined by the forum. This typically will use a database 173 | * session and query the database for the required information. The data is returned to the 174 | * main handler to be cached and used. 175 | * 176 | * @param int $id_member The id of the member to load from the database. This is determiend by the cookie. 177 | * @return array All data from the forum database. 178 | */ 179 | public function getForumMember(int $id_member): array 180 | { 181 | $result = $this->db->query(' 182 | SELECT 183 | id_member, 184 | member_name, email_address, real_name, passwd, password_salt, 185 | id_group, id_post_group, additional_groups, 186 | member_ip, member_ip2 187 | FROM ' . $this->ForumSettings['db_prefix'] . 'members 188 | WHERE 189 | id_member = ' . (int) $id_member . ' 190 | AND is_activated = 1 191 | LIMIT 1'); 192 | $member = $this->db->fetch_assoc($result); 193 | $this->db->free($result); 194 | 195 | return (array) $member; 196 | } 197 | 198 | /* 199 | * Retrieves the member ID as defined by the forum software. Typically this is the same as the ID provided 200 | * by the cookie but may differ depending on forum softwares. 201 | * 202 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 203 | depend on the data from getForumMember having existed due to caching. 204 | * @return int The ID of the member from the forum software. 205 | */ 206 | public function getMemberID(array $member): int 207 | { 208 | return (int) $member['id_member']; 209 | } 210 | 211 | /* 212 | * Retrieves the member display name as defined by the forum software. 213 | * 214 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 215 | depend on the data from getForumMember having existed due to caching. 216 | * @return string The member display name. 217 | */ 218 | public function getMemberName(array $member): string 219 | { 220 | return (string) $member['member_name']; 221 | } 222 | 223 | /* 224 | * Retrieves the member groups as defined by the forum software. 225 | * 226 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 227 | depend on the data from getForumMember having existed due to caching. 228 | * @return array An array of intergers of the forum group ids this member is apart of. 229 | */ 230 | public function getMemberGroups(array $member): array 231 | { 232 | $groups = array( 233 | (int) $member['id_group'], 234 | (int) $member['id_post_group'] 235 | ); 236 | 237 | if (!empty($member['additional_groups'])) 238 | $groups = array_merge($groups, array_map('intval', explode(',', $member['additional_groups']))); 239 | 240 | return $groups; 241 | } 242 | 243 | /* 244 | * Retrieves the member email address as defined by the forum software. 245 | * 246 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 247 | depend on the data from getForumMember having existed due to caching. 248 | * @return string The member email address. 249 | */ 250 | public function getMemberEmailAddress(array $member): string 251 | { 252 | return (string) $member['email_address']; 253 | } 254 | 255 | /* 256 | * Retrieves the member login name as defined by the forum software. Typically this is the same as the ID provided 257 | * by the cookie but may differ depending on forum softwares. 258 | * 259 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 260 | depend on the data from getForumMember having existed due to caching. 261 | * @return string The member login name. 262 | */ 263 | public function getMemberRealName(array $member): string 264 | { 265 | return (string) $member['real_name']; 266 | } 267 | 268 | /* 269 | * SMF Auth used a different option to track for account take overs. This converts it to the method used 270 | * by this extension. 271 | * 272 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 273 | * depend on the data from getForumMember having existed due to caching. 274 | * @param object $wikiuser The wiki user object provided for updating options. Note while getOption/setOption 275 | * are deprecated, we still use them here as this legacy option should not be required in the future. 276 | * @return bool True if we had to convert the setting and update the user, false otherwise. 277 | */ 278 | public function legacyUpdateWikiUser(array $member, object $wikiUser): bool 279 | { 280 | // Convert the smf_member_id over? 281 | if ($this->ForumSettings['NameStyle'] !== 'smf') 282 | return false; 283 | 284 | if (empty($this->wikiMemberOptions) || !is_object($this->wikiMemberOptions)) 285 | $this->wikiMemberOptions = \MediaWiki\MediaWikiServices::getInstance()->getUserOptionsManager(); 286 | 287 | if ($this->wikiMemberOptions->getIntOption($wikiUser, 'forum_member_id', 0) == 0 && $this->wikiMemberOptions->getIntOption($wikiUser, 'smf_member_id', 0) != 0) 288 | { 289 | $this->MWlogger->debug('SMF Auth conversion to FS Provider. "{OLD}" vs "{NEW}"', array( 290 | 'OLD' => $this->wikiMemberOptions->getIntOption($wikiUser, 'forum_member_id', 0), 291 | 'NEW' => $this->wikiMemberOptions->getIntOption($wikiUser, 'smf_member_id', 0), 292 | )); 293 | $this->wikiMemberOptions->setOption($wikiUser, 'forum_member_id', (int) $member['id_member']); 294 | $this->wikiMemberOptions->setOption($wikiUser, 'smf_member_id', 0); 295 | return true; 296 | } 297 | 298 | return false; 299 | } 300 | 301 | /* 302 | * Check if the member is banned in the forum software. If so let the SSO know to prevent them from 303 | * logging into the wiki. 304 | * 305 | * We first check to see if the is_activated is >= 10, which in SMF indicates they are banned. 306 | * We then check the bans table to see if their is another ban to validate against incase their profile does not reflect the ban. 307 | * We then check to see if their email. This is used incase the email has changed but the ban rule is not updated. 308 | * We then check to see if their IP matches. This is used to ensure IP bans activate as expected. 309 | * 310 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 311 | depend on the data from getForumMember having existed due to caching. 312 | * @return bool True if banned, false otherwise. 313 | */ 314 | public function checkBans(array $member): bool 315 | { 316 | $banned = isset($member['is_activated']) ? $member['is_activated'] >= 10 : 0; 317 | 318 | if (empty($banned)) 319 | $banned = $this->__check_basic_ban((int) $member['id_member']); 320 | 321 | if (empty($banned)) 322 | $banned = $this->__check_email_ban((string) $member['email_address']); 323 | 324 | if (empty($banned)) 325 | { 326 | $ips = array( 327 | $member['ip'], 328 | $member['ip2'], 329 | $_SERVER['REMOTE_ADDR'], 330 | ); 331 | 332 | // SMF 2.0 only supports IPv4. 333 | foreach ($ips as $ip) 334 | if (preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $ip_parts) == 1) 335 | { 336 | $banned = $this->__check_ip_ban($ip, $ip_parts); 337 | 338 | if ($banned) 339 | continue; 340 | } 341 | } 342 | 343 | return $banned; 344 | } 345 | 346 | /* 347 | * Check if the member is banned in the forum software by checking for the basic ban member. 348 | * 349 | * @param int $id_member The id of the member provided by the forum software. 350 | * @return bool True if banned, false otherwise. 351 | */ 352 | protected function __check_basic_ban(int $id_member): bool 353 | { 354 | $request = $this->db->query(' 355 | SELECT id_ban 356 | FROM ' . $this->ForumSettings['db_prefix'] . 'ban_items AS bi 357 | LEFT JOIN ' . $this->ForumSettings['db_prefix'] . 'ban_groups AS bg 358 | ON (bi.id_ban_group = bg.id_ban_group) 359 | WHERE bi.id_member = ' . $id_member . ' 360 | AND (bg.cannot_post = 1 OR bg.cannot_login = 1)'); 361 | 362 | $banned = (int) $this->db->fetch_assoc($request); 363 | $this->db->free($request); 364 | 365 | return $banned !== 0; 366 | } 367 | 368 | 369 | /* 370 | * Check if the member is banned in the forum software by checking for the email address. 371 | * 372 | * @param string $email_address The forum members email address. 373 | * @return bool True if banned, false otherwise. 374 | */ 375 | protected function __check_email_ban(string $email_address): bool 376 | { 377 | $request = $this->db->query(' 378 | SELECT id_ban 379 | FROM ' . $this->ForumSettings['db_prefix'] . 'ban_items AS bi 380 | LEFT JOIN ' . $this->ForumSettings['db_prefix'] . 'ban_groups AS bg 381 | ON (bi.id_ban_group = bg.id_ban_group) 382 | WHERE "' . $this->db->quote($email_address) . '" LIKE bi.email_address 383 | AND (bg.cannot_post = 1 OR bg.cannot_login = 1)'); 384 | 385 | $banned = (int) $this->db->fetch_assoc($request); 386 | $this->db->free($request); 387 | 388 | return $banned !== 0; 389 | } 390 | 391 | /* 392 | * Check if the member is banned in the forum software by checking for the IP address. 393 | * 394 | * @param string $ip The forum members email address. 395 | * @param array $ip_parts The IP exploded. 396 | * @return bool True if banned, false otherwise. 397 | */ 398 | protected function __check_ip_ban(string $ip, array $ip_parts): bool 399 | { 400 | $request = $this->db->query(' 401 | SELECT id_ban 402 | FROM ' . $this->ForumSettings['db_prefix'] . 'ban_items AS bi 403 | LEFT JOIN ' . $this->ForumSettings['db_prefix'] . 'ban_groups AS bg 404 | ON (bi.id_ban_group = bg.id_ban_group) 405 | WHERE ((' . (int) $ip_parts[1] . ' BETWEEN bi.ip_low1 AND bi.ip_high1) 406 | AND (' . (int) $ip_parts[2] . ' BETWEEN bi.ip_low2 AND bi.ip_high2) 407 | AND (' . (int) $ip_parts[3] . ' BETWEEN bi.ip_low3 AND bi.ip_high3) 408 | AND (' . (int) $ip_parts[4] . ' BETWEEN bi.ip_low4 AND bi.ip_high4)) 409 | AND (bg.cannot_post = 1 OR bg.cannot_login = 1)'); 410 | 411 | $banned = (int) $this->db->fetch_assoc($request); 412 | $this->db->free($request); 413 | 414 | return $banned !== 0; 415 | } 416 | } -------------------------------------------------------------------------------- /ForumProvider/smf2.1.php: -------------------------------------------------------------------------------- 1 | 'signup', 26 | 'userlogin' => 'login', 27 | 'userlogout' => 'logout' 28 | ]; 29 | 30 | /** 31 | * Decodes the forum software cookie returning the id and password. 32 | * 33 | * @return array The ID and password. 34 | */ 35 | public function decodeCookie(): array 36 | { 37 | return (array) json_decode($_COOKIE[$this->ForumSettings['cookiename']], true); 38 | } 39 | 40 | /* 41 | * Figure out the URL that we need to send the user to in order to perform the requested action. 42 | * The forum software should process the action and then return to MediaWiki. 43 | */ 44 | public function getRedirectURL(string $action, string $wiki_url, bool $do_return = false): string 45 | { 46 | $forum_action = $this->validRedirectActions[$action]; 47 | 48 | $forum_url = 49 | $this->ForumSettings['boardurl'] 50 | . '/index.php?action=' . $forum_action 51 | . ';return_hash=' . hash_hmac('sha1', $wiki_url, $this->ForumSettings['auth_secret']) 52 | . ';return_to=' . urlencode($wiki_url); 53 | 54 | return $forum_url; 55 | } 56 | 57 | /* 58 | * Validate that the cookie has a valid password for the user. This should ensure 59 | * that forged cookies or if a password has changed that the cookie rejects the login. 60 | * 61 | * @param array $user The user data. 62 | * @param array $cookie The cookie data. 63 | * @return bool True if the cookie is valid, false otherwise. 64 | */ 65 | public function cookiePasswordIsValid(array $user, array $cookie): bool 66 | { 67 | if (empty($user) || empty($cookie)) 68 | return false; 69 | 70 | if (!empty($this->ForumSettings['auth_secret']) && empty($this->ForumSettings['cookie_no_auth_secret'])) 71 | return hash_equals(hash_hmac('sha512', $user['passwd'], $this->ForumSettings['auth_secret'] . $user['password_salt']), $cookie['password']); 72 | else 73 | return $cookie['password'] === hash('sha512', $user['passwd'] . $user['password_salt']); 74 | } 75 | 76 | /* 77 | * Check if the member is banned in the forum software. If so let the SSO know to prevent them from 78 | * logging into the wiki. 79 | * 80 | * We first check to see if the is_activated is >= 10, which in SMF indicates they are banned. 81 | * We then check the bans table to see if their is another ban to validate against incase their profile does not reflect the ban. 82 | * We then check to see if their email. This is used incase the email has changed but the ban rule is not updated. 83 | * We then check to see if their IP matches. This is used to ensure IP bans activate as expected. 84 | * 85 | * @param array $member The set of forum member data previsouly returned by getForumMember. Do not 86 | depend on the data from getForumMember having existed due to caching. 87 | * @return bool True if banned, false otherwise. 88 | */ 89 | public function checkBans(array $member): bool 90 | { 91 | $banned = isset($member['is_activated']) ? $member['is_activated'] >= 10 : 0; 92 | 93 | if (empty($banned)) 94 | $banned = $this->__check_basic_ban((int) $member['id_member']); 95 | 96 | if (empty($banned)) 97 | $banned = $this->__check_email_ban((string) $member['email_address']); 98 | 99 | if (empty($banned)) 100 | { 101 | $ips = array( 102 | $member['member_ip'], 103 | $member['member_ip2'], 104 | $_SERVER['REMOTE_ADDR'], 105 | ); 106 | 107 | // SMF 2.0 only supports IPv4. 108 | foreach ($ips as $ip) 109 | { 110 | $ip_parts = array(); 111 | preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $ip_parts); 112 | 113 | $banned = $this->__check_ip_ban($ip, $ip_parts); 114 | 115 | if ($banned) 116 | continue; 117 | } 118 | } 119 | 120 | return $banned; 121 | } 122 | 123 | /* 124 | * Check if the member is banned in the forum software by checking for the IP address. 125 | * 126 | * @param string $ip The forum members email address. 127 | * @return bool True if banned, false otherwise. 128 | */ 129 | protected function __check_ip_ban(string $ip, array $ip_parts): bool 130 | { 131 | // If we have IP parts, we have IPv4 address. 132 | if (!empty($ip_parts)) 133 | $ip_bin = bin2hex(inet_pton($ip)); 134 | else 135 | $ip_bin = bin2hex($ip); 136 | 137 | $sql = ' 138 | SELECT id_ban 139 | FROM ' . $this->ForumSettings['db_prefix'] . 'ban_items AS bi 140 | LEFT JOIN ' . $this->ForumSettings['db_prefix'] . 'ban_groups AS bg 141 | ON (bi.id_ban_group = bg.id_ban_group)'; 142 | 143 | // Postgresql uses ::inet 144 | if ($this->db->getDbType() === 'postgresql') 145 | $sql .= ' 146 | WHERE ("' . (string) $this->db->quote($ip_bin) . '"::inet) BETWEEN bi.ip_low AND bi.ip_high)'; 147 | else 148 | $sql .= ' 149 | WHERE (unhex("' . (string) $this->db->quote($ip_bin) . '") BETWEEN bi.ip_low AND bi.ip_high)'; 150 | 151 | $sql .= ' 152 | AND (bg.cannot_post = 1 OR bg.cannot_login = 1)'; 153 | 154 | $request = $this->db->query($sql); 155 | $banned = (int) $this->db->fetch_assoc($request); 156 | $this->db->free($request); 157 | 158 | return $banned !== 0; 159 | } 160 | } -------------------------------------------------------------------------------- /ForumSsoProvider.php: -------------------------------------------------------------------------------- 1 | MWlogger = \MediaWiki\Logger\LoggerFactory::getInstance('ForumSessionProvider'); 73 | $this->MWlogger->debug('Constructor initialized.'); 74 | 75 | // Set our software up. 76 | $this->ForumSoftware = !empty($wgSMFLogin) && empty($wgFSPSoftware) ? 'smf2.0' : (!empty($wgFSPSoftware) ? $wgFSPSoftware : null); 77 | 78 | // Load our settings. 79 | $this->wikiScriptPath = $wgScriptPath; 80 | $this->loadFSSettings(); 81 | 82 | // Load up the correct forum software provider. 83 | $forumClass = 'ForumSoftwareProvider' . str_replace('.', '', $this->ForumSoftware); 84 | $this->fs = new $forumClass($this->MWlogger, $this->db, $this->ForumSettings); 85 | 86 | // Is this a legacy authentication plugin?. 87 | if (!empty($wgSMFLogin) && method_exists($this->fs, 'compatLegacy')) 88 | $this->fs->compatLegacy(); 89 | 90 | // Make sure we can find the settings file. 91 | if ($this->fs->configurationFileIsValid($this->ForumSettings['path'])) 92 | { 93 | $this->MWlogger->debug('Found Configuration File, attempting to loading.'); 94 | 95 | // Read the Settings file in, use this layer to adjust what we need to bring in. 96 | $this->fs->readConfigurationFile($this->ForumSettings['path']); 97 | 98 | // Read the cookie 99 | $this->decodeCookie(); 100 | 101 | // If we have a valid ID, lets connect to the database. 102 | if (!empty($this->ForumCookie['id']) && is_integer($this->ForumCookie['id'])) 103 | { 104 | $this->MWlogger->debug('User detected, attempting to load the database.'); 105 | 106 | $this->setupDatabaseProvider(); 107 | } 108 | else 109 | $this->MWlogger->debug('No User detected, fall through to MediaWiki.'); 110 | } 111 | else 112 | { 113 | $this->MWlogger->debug('Configuration File missing or not readable. Tried to load at {path}', array('path' => $this->ForumSettings['path'])); 114 | $this->MWlogger->warning('Forum Software Integraiton invalid.'); 115 | } 116 | } 117 | 118 | /** 119 | * MediaWiki will call t his when loading a special page. We only need to grab a few pages 120 | * and redirect them to the forum for handling. These are login, logout and registering a new account. 121 | * This just returns to the objecct via $wgForumSessionProviderInstance and calls doRedirect; 122 | * 123 | * @param object $special The special page called. 124 | * @param string|null $subPage Subpage string, or null if no subpage was specified 125 | * @hook MediaWiki\SpecialPage\Hook\SpecialPageBeforeExecuteHook::onSpecialPageBeforeExecute 126 | * @return void If this matches, we issue a redirect, otherwise we return nothing. 127 | */ 128 | public static function onSpecialPageBeforeExecute($special, $subPage): void 129 | { 130 | global $wgForumSessionProviderInstance; 131 | 132 | // Ensure its callable. 133 | if (!is_callable(array($wgForumSessionProviderInstance, 'doRedirect'))) 134 | return; 135 | 136 | // The case of some of these isn't always consistent with what shows up in the url. 137 | $special_action = strtolower($special->getName()); 138 | 139 | // If this is a valid action, let the redirector know. 140 | if (in_array($special_action, array('createaccount', 'userlogin', 'userlogout'))) 141 | $wgForumSessionProviderInstance->doRedirect($special_action, true); 142 | } 143 | 144 | /** 145 | * Actually do the redirect. We setup where we are at in the wiki and then ask the forum software 146 | * to handle redirecting us. The forum software is responsible for handling the action and returning 147 | * to the proper location in MediaWiki. 148 | * 149 | * @param string $action The action we are calling. 150 | * @param bool $do_return if we should return or not. 151 | * @return void We will be doing a redirect and exiting execution here. 152 | */ 153 | public function doRedirect(string $action, bool $do_return = false): void 154 | { 155 | global $wgScriptPath; 156 | 157 | // The wiki URL. 158 | $page = !empty($_GET['returnto']) ? '?title=' . $_GET['returnto'] . '&' : '?'; 159 | $wiki_url = 'http://' . $_SERVER['SERVER_NAME'] . $wgScriptPath . '/index.php' . $page . 'board=redirect'; 160 | 161 | // Send this to the forum handler to give us the proper redirect url. 162 | $redirect_url = $this->fs->getRedirectURL($action, $wiki_url, $do_return); 163 | 164 | // Redirect and leave this. 165 | header ('Location: ' . $redirect_url); 166 | exit; 167 | } 168 | 169 | /** 170 | * Sets up the the session for MediaWiki and returns it. 171 | * MediaWiki will call this directly when it is ready to load up the user. 172 | * This will validate the user is logged into the forum, perform any updates and ban checks, 173 | * then let MediaWiki know this session is valid. 174 | * 175 | * @param WebRequest The request information provided by MediaWIki. 176 | * @return SessionInfo|null A valid session handler is returned if the user is logged in, otherwise null. 177 | */ 178 | public function provideSessionInfo(WebRequest $request) 179 | { 180 | // Can't do this without a database connection, they are a guest now. 181 | if (empty($this->db) || empty($this->db->isLoaded())) 182 | { 183 | $this->MWlogger->debug('Unable to provide session, database not loaded.'); 184 | return null; 185 | } 186 | else 187 | $this->MWlogger->debug('Database loaded, attempting to load forum member.'); 188 | 189 | // Fetch the user. 190 | $this->ForumMember = $this->getForumMember($request); 191 | 192 | // Can't find this member. 193 | if (empty($this->ForumMember)) 194 | { 195 | $this->MWlogger->debug('Member id, {FSID}, not found in forum database', array('FSID' => $this->ForumCookie['id'])); 196 | return null; 197 | } 198 | else 199 | $this->MWlogger->debug('Forum member found, verifying cookie of {FSID}', array('FSID' => $this->ForumCookie['id'])); 200 | 201 | // Password not valid? 202 | if (!$this->fs->cookiePasswordIsValid($this->ForumMember, $this->ForumCookie)) 203 | { 204 | $this->MWlogger->debug('Member ID, {FSID}, failed to validate password under {USERIP}', array( 205 | 'FSID' => $this->ForumCookie['id'], 206 | 'USERIP' => $_SERVER['REMOTE_ADDR'], 207 | )); 208 | return null; 209 | } 210 | else 211 | $this->MWlogger->debug('Member found and verified, verifying access.'); 212 | 213 | // Cleanup the username. 214 | $this->ForumMemberNameCleaned = $this->cleanupUserName($this->fs->getMemberName($this->ForumMember)); 215 | 216 | // Invalid name? 217 | if (is_null($this->ForumMemberNameCleaned)) 218 | { 219 | $this->MWlogger->debug('Invalid username, aborting integraiton.'); 220 | return null; 221 | } 222 | 223 | // Get all of our Forum Software groups. 224 | $this->ForumMemberGroups = $this->fs->getMemberGroups($this->ForumMember); 225 | 226 | // Try to access this user. 227 | $this->MWlogger->debug('Attempting to locate a valid user in MediaWiki or create one if it does not exist'); 228 | $this->wikiUserInfo = \MediaWiki\Session\UserInfo::newFromName($this->ForumMemberNameCleaned, true); 229 | $this->wikiMember = $this->wikiUserInfo->getUser(); 230 | 231 | // If they are not logged in or the username doesnt match. 232 | if (!($this->wikiMember->isRegistered() && $this->wikiMember->getName() === $this->ForumMemberNameCleaned)) 233 | { 234 | $this->MWlogger->debug('Attempting to login a mediawiki user, if the user does not exist, this fails silently.'); 235 | 236 | $this->wikiMember->setId(\MediaWiki\MediaWikiServices::getInstance()->getUserIdentityLookup()->getUserIdentityByName($this->ForumMemberNameCleaned)); 237 | 238 | // The user doesn't exist yet in the wiki? Create them. 239 | if ($this->wikiMember->getID() === 0) 240 | { 241 | $this->MWlogger->debug('User does not exist, attemtping to create it.'); 242 | $this->createWikiUser(); 243 | } 244 | } 245 | 246 | // Make sure if we have a id match, its valid. 247 | if ($this->getUserOption('forum_member_id') !== 0 && $this->getUserOption('forum_member_id') !== $this->ForumCookie['id']) 248 | { 249 | $this->MWlogger->debug('Member ID, {FSID}, failed to match forum provider check under {USERIP}', array( 250 | 'FSID' => $this->ForumCookie['id'], 251 | 'USERIP' => $_SERVER['REMOTE_ADDR'], 252 | )); 253 | return null; 254 | } 255 | else 256 | $this->MWlogger->debug('Forum Provider check validated.'); 257 | 258 | // Check the ban status here. 259 | if ($this->memberIsBannedOnForum()) 260 | { 261 | $this->MWlogger->debug('Member was matched as banned.'); 262 | return null; 263 | } 264 | 265 | // Configure all of our groups, but only every 15 minutes. 266 | if (time() > ((int) $this->getUserOption('forum_last_update_groups') + $this->update_groups_interval)) 267 | $this->updateWikiUserGroups(); 268 | 269 | // If any user data has changed, go ahead and update it now. 270 | $this->updateWikiUser(); 271 | 272 | // Denied Login? 273 | if ( 274 | !empty($this->ForumSettings['LoginDeniedGroups']) 275 | && is_array($this->ForumSettings['LoginDeniedGroups']) 276 | && array_intersect($this->ForumSettings['LoginDeniedGroups'], $this->ForumMemberGroups) !== array() 277 | ) 278 | { 279 | $this->MWlogger->debug('Member was found in Login Deny Groups, rejected...'); 280 | return null; 281 | } 282 | 283 | // Not apart of a login group? 284 | $tempGroups = (array) $this->ForumSettings['LoginAllowedGroups']; 285 | $tempGroups += (array) $this->ForumSettings['AdminGroups']; 286 | if (!empty($this->ForumSettings['LoginAllowedGroups']) && array_intersect($tempGroups, $this->ForumMemberGroups) === array()) 287 | { 288 | $this->MWlogger->debug('Member is not apart of any login groups...'); 289 | return null; 290 | } 291 | 292 | $this->MWlogger->debug('Everything is valid, returning valid session for wiki...'); 293 | 294 | // This was in the original code and sessionCookieName is not defined anywhere. 295 | if ($this->sessionCookieName === null) 296 | { 297 | $id = $this->hashToSessionId($this->ForumMemberNameCleaned); 298 | $persisted = false; 299 | $forceUse = true; 300 | } 301 | else 302 | { 303 | $id = $this->getSessionIdFromCookie($request); 304 | $persisted = $id !== null; 305 | $forceUse = false; 306 | } 307 | 308 | // Stand up a new session for MediaWiki. 309 | return new \MediaWiki\Session\SessionInfo(\MediaWiki\Session\SessionInfo::MAX_PRIORITY, array( 310 | 'provider' => $this, 311 | 'id' => $id, 312 | 'userInfo' => $this->wikiUserInfo, 313 | 'persisted' => $persisted, 314 | 'forceUse' => $forceUse, 315 | )); 316 | } 317 | 318 | /** 319 | * Load up all MediaWiki settings for the Forum Session Provider extension. 320 | * This will simply passt them into a localized array for processing later. 321 | * 322 | * @return void No return is expected. 323 | */ 324 | private function loadFSSettings(): void 325 | { 326 | global $wgFSPPath, $wgFSPDenyGroups, $wgFSPAllowGroups, $wgFSPAdminGroups, $wgFSPSuperGroups, $wgFSPInterfaceGroups, $wgFSPSpecialGroups, $wgFSPNameStyle, $wgFSPEnableBanCheck; 327 | 328 | $this->MWlogger->debug('Loading Forum System Settings.'); 329 | 330 | // Some standard settings and if they do not exist, provide a default. 331 | $this->ForumSettings['path'] = isset($wgFSPPath) ? $wgFSPPath : '../forum'; 332 | $this->ForumSettings['NameStyle'] = !empty($wgFSPNameStyle) ? strtolower($wgFSPNameStyle) : 'default'; 333 | $this->ForumSettings['EnableBanCheck'] = !empty($wgFSPEnableBanCheck) ? true : false; 334 | $this->ForumSettings['ForumDatabaseProvider'] = !empty($wgFSPDatabaseProvider) ? strtolower($wgFSPDatabaseProvider) : 'mysql'; 335 | 336 | // Bring grous in, if they do not exist, default to a empty array. 337 | foreach (array( 338 | 'LoginDeniedGroups' => 'wgFSPDenyGroups', 339 | 'LoginAllowedGroups' => 'wgFSPAllowGroups', 340 | 'AdminGroups' => 'wgFSPAdminGroups', 341 | 'SuperAdminGroups' => 'wgFSPSuperGroups', 342 | 'InterfaceGroups' => 'wgFSPInterfaceGroups', 343 | 'SpecialGroups' => 'wgFSPSpecialGroups', 344 | ) as $key => $value) 345 | $this->ForumSettings[$key] = !empty($$value) ? $$value : array(); 346 | } 347 | 348 | /** 349 | * Decode the forum software cookies. 350 | * We wil handle some basics here then send off to the forum software provider 351 | * to do the decoding and reeturn a id and password to be validated later. 352 | * We ensure that the ID is a int and password a string. 353 | * 354 | * @return void No return is expected. 355 | */ 356 | private function decodeCookie(): void 357 | { 358 | // Set the defaults. 359 | $this->ForumCookie['id'] = 0; 360 | $this->ForumCookie['password'] = null; 361 | 362 | $this->MWlogger->debug('Loading the cookie using provider: {software}', array('software' => $this->ForumSoftware)); 363 | 364 | // No cookie? No luck! 365 | if (!$this->fs->cookieExists()) 366 | { 367 | $this->MWlogger->debug('No Cookie present, aborting integration.'); 368 | return; 369 | } 370 | 371 | // This should validate the cookie and return the id/password. 372 | list($this->ForumCookie['id'], $this->ForumCookie['password']) = $this->fs->decodeCookie(); 373 | 374 | $this->ForumCookie['id'] = (int) $this->ForumCookie['id']; 375 | $this->ForumCookie['password'] = (string) $this->ForumCookie['password']; 376 | 377 | $this->MWlogger->debug('Read the cookie, possible member ID "{FSID}" found', array('FSID' => $this->ForumCookie['id'])); 378 | } 379 | 380 | /** 381 | * Sets up a database connection in the forum software. 382 | * If we are using MySQL(i) and have the mysqli class avaiaiable, we use it, otherwise 383 | * we simply use the generic PDO handler. 384 | * We pass on the logger object handler and the current database type to the class. 385 | * Database type should be mysql, mysqli or postgresql. 386 | * 387 | * @return void No return is expected. 388 | */ 389 | private function setupDatabaseProvider(): void 390 | { 391 | if ( 392 | (!empty($this->ForumSettings['ForumDatabaseProvider']) && $this->ForumSettings['ForumDatabaseProvider'] == 'mysql') 393 | || ($this->ForumSettings['db_type'] === 'mysql' && class_exists('mysqli')) 394 | ) 395 | $databaseClass = 'ForumDatabaseProviderMySQLi'; 396 | else 397 | $databaseClass = 'ForumDatabaseProviderPDO'; 398 | 399 | $this->db = new $databaseClass($this->MWlogger, $this->ForumSettings['db_type']); 400 | 401 | $this->db->connect($this->ForumSettings['db_server'], $this->ForumSettings['db_user'], $this->ForumSettings['db_passwd'], $this->ForumSettings['db_name']); 402 | } 403 | 404 | /** 405 | * Fetch the Forum Member information from the forum software database. 406 | * This will attempt to cache this information for future usage to reduce queries against 407 | * our forum software database. This information is cached at the interval provided. 408 | * 409 | * @return array All the data provided by the forum software for this specific member. 410 | */ 411 | private function getForumMember(WebRequest $request): array 412 | { 413 | // Simple caching? 414 | try 415 | { 416 | if (method_exists(\MediaWiki\MediaWikiServices::getInstance(), 'getLocalServerObjectCache')) 417 | $cache = \MediaWiki\MediaWikiServices::getInstance()->getLocalServerObjectCache(); 418 | } catch (MWException $e) { 419 | } 420 | 421 | // Use another caching method. 422 | if (!is_object($cache)) 423 | $cache = new EmptyBagOStuff(); 424 | 425 | // See if this queue is in Cache, makeKey uses wiki id, but not member id. 426 | if (is_object($cache)) 427 | $key = $cache->makeKey( 428 | 'SessionProviders', 429 | 'ForumSessionProvider_' . ((int) $this->ForumCookie['id']) . filemtime(__FILE__) 430 | ); 431 | 432 | // Attempt to retrieve this from the cache. 433 | $data = $cache->get($key); 434 | if (!empty($data)) 435 | { 436 | $this->MWlogger->debug('Found a cached instance of this data, using it'); 437 | $this->ForumMember = (array) $data; 438 | return (array) $data; 439 | } 440 | 441 | $this->MWlogger->debug('Querying Forum Provider for member data'); 442 | 443 | // Ask the forum software for the information. 444 | $this->ForumMember = $this->fs->getForumMember((int) $this->ForumCookie['id']); 445 | 446 | // Cache this up. 447 | if (is_object($cache)) 448 | $cache->set($key, $this->ForumMember, $this->forum_member_cache_interval); 449 | 450 | return $this->ForumMember; 451 | } 452 | 453 | /** 454 | * Cleans up a username to a specific format and returns the cleaned up name for use later. 455 | * Methods are: 456 | * smf: Cleans name by replacing characters incompatible in MediaWiki with characters invalid in SMF. 457 | * domain: Validates name matches a standard ASCII character set, rejects them if not. 458 | * default: Validates name matches a usable username by rejecting their name if it contains invalid MediaWiki characters. 459 | * 460 | * @param string $userName The original username from the forum. 461 | * @return string|null The cleaned name or if invalid null. 462 | */ 463 | private function cleanupUserName(string $userName): string 464 | { 465 | $this->MWlogger->debug('Cleanup name "{FSNAME}" using method {FSMMETHOD}', array( 466 | 'FSNAME' => $userName, 467 | 'FSMMETHOD' => strtolower($this->ForumSettings['NameStyle']), 468 | )); 469 | 470 | $userName = ucfirst($userName); 471 | 472 | // Does the forum provider have method we want to use. 473 | if (method_exists($this->fs, 'cleanupUserName')) 474 | { 475 | $userName = $this->fs->cleanupUserName($userName); 476 | 477 | // If we told false, we know to fail through, otherwise we will continue on below. 478 | if ($userName === false) 479 | return null; 480 | } 481 | 482 | switch (strtolower($this->ForumSettings['NameStyle'])) 483 | { 484 | case 'smf': 485 | // Generally backwards compatible with former SMF/Elkarte Auth plugins. 486 | $userName = str_replace('_', '\'', $userName); 487 | $userName = strtr($userName, array('[' => '=', ']' => '"', '|' => '&', '#' => '\\', '{' => '==', '}' => '""', '@' => '&&', ':' => '\\\\')); 488 | break; 489 | case 'domain': 490 | // A more restrictive policy. 491 | if ($userName !== preg_replace('`[^a-zA-Z0-9 .-]+`i', '', $userName)) 492 | return null; 493 | break; 494 | default: 495 | // Just kick them if they have an unusable username. 496 | if (preg_match('`[#<>[\]|{}@:]+`', $userName)) 497 | return null; 498 | } 499 | 500 | $this->MWlogger->debug('Cleanuped name "{FSNAME}"', array('FSNAME' => $userName)); 501 | 502 | return $userName; 503 | } 504 | 505 | /** 506 | * Check if the name or email needs updated. If so, we instruct MediaWiki to save the changes 507 | * to MediaWiki. If we have any legacy checks to make, we ask the forum software provider to 508 | * make those. 509 | * 510 | * @return void No return is expected. 511 | */ 512 | private function updateWikiUser(): void 513 | { 514 | $this->MWlogger->debug('Updating wiki user.'); 515 | 516 | $userChanged = false; 517 | if ($this->wikiMember->getEmail() !== $this->fs->getMemberEmailAddress($this->ForumMember)) 518 | { 519 | $this->MWlogger->debug('Email Sync Reequired. "{OLD}" vs "{NEW}"', array( 520 | 'OLD' => $this->wikiMember->getEmail(), 521 | 'NEW' => $this->fs->getMemberEmailAddress($this->ForumMember), 522 | )); 523 | 524 | $this->wikiMember->setEmail($this->fs->getMemberEmailAddress($this->ForumMember)); 525 | $this->wikiMember->mEmailAuthenticated = wfTimestampNow(); 526 | $userChanged = true; 527 | } 528 | 529 | if ($this->wikiMember->getRealName() !== $this->fs->getMemberRealName($this->ForumMember)) 530 | { 531 | $this->MWlogger->debug('Real Name Sync Reequired. "{OLD}" vs "{NEW}"', array( 532 | 'OLD' => $this->wikiMember->getRealName(), 533 | 'NEW' => $this->fs->getMemberRealName($this->ForumMember), 534 | )); 535 | 536 | $this->wikiMember->setRealName($this->fs->getMemberRealName($this->ForumMember)); 537 | $userChanged = true; 538 | } 539 | 540 | // Do we have a legacy update to make? 541 | if (method_exists($this->fs, 'legacyUpdateWikiUser')) 542 | $userChanged |= $this->fs->legacyUpdateWikiUser($this->ForumMember, $this->wikiMember); 543 | 544 | // No need to save if nothing has happened 545 | if ($userChanged) 546 | { 547 | $this->MWlogger->debug('Saved wiki user changes.'); 548 | 549 | $this->setUserOption('forum_last_update_user', time()); 550 | $this->wikiMember->saveSettings(); 551 | } 552 | else 553 | $this->MWlogger->debug('No changes to sync.'); 554 | } 555 | 556 | /** 557 | * Check if our member needs added or removed from specific groups to update 558 | * the members access to MediaWiki. This uses the standard group settings but also 559 | * allows for extending to customized groups or non standard groups. 560 | * 561 | * @return void No return is expected. 562 | */ 563 | private function updateWikiUserGroups(): void 564 | { 565 | $this->MWlogger->debug('Updating wiki groups...'); 566 | $this->MWlogger->debug('Current Forum Member Groups:' .implode(',', $this->ForumMemberGroups) . '...'); 567 | $this->MWlogger->debug('Current Wiki Effective Groups:' .implode(',', $this->getUserEffectiveGroups()) . '...'); 568 | 569 | // Wiki Group Name => Forum Group IDS 570 | $groupActions = array( 571 | 'sysop' => $this->ForumSettings['AdminGroups'], 572 | 'interface-admin' => $this->ForumSettings['InterfaceGroups'], 573 | 'bureaucrat' => $this->ForumSettings['SuperAdminGroups'], 574 | ); 575 | 576 | // Add in our special groups. 577 | foreach ($this->ForumSettings['SpecialGroups'] as $fs_group_id => $wiki_group_name) 578 | { 579 | // Group didn't exist? 580 | if (!isset($groupActions[$wiki_group_name])) 581 | $groupActions[$wiki_group_name] = array(); 582 | 583 | // Add the Forum group into the wiki group. 584 | $groupActions[$wiki_group_name][] = $fs_group_id; 585 | } 586 | 587 | // Now we are going to check all the groups, ignoring updating if nothing has changed. 588 | $madeChange = false; 589 | foreach ($groupActions as $wiki_group_name => $fs_group_ids) 590 | { 591 | // No group ids, skip. 592 | if (empty($fs_group_ids) || $fs_group_ids == array()) 593 | { 594 | $this->MWlogger->debug('Skipping ' . $wiki_group_name . ' due to no forum mappings...'); 595 | continue; 596 | } 597 | 598 | // They are in the Forum group but not the wiki group? 599 | if ( 600 | array_intersect($fs_group_ids, $this->ForumMemberGroups) != array() 601 | && !in_array($wiki_group_name, $this->getUserEffectiveGroups()) 602 | ) 603 | { 604 | $this->MWlogger->debug('Adding ' . $wiki_group_name . ' as member is apart of forum group which grants access (' . implode(',', $fs_group_ids) . ')...'); 605 | 606 | $this->wikiMember->addGroup($wiki_group_name); 607 | $madeChange = true; 608 | } 609 | // They are not in the Forum group, but in the wiki group 610 | elseif ( 611 | array_intersect($fs_group_ids, $this->ForumMemberGroups) == array() 612 | && in_array($wiki_group_name, $this->getUserEffectiveGroups()) 613 | ) 614 | { 615 | $this->MWlogger->debug('Removing ' . $wiki_group_name . ' as member is no longer apart of forum group which grants access (' . implode(',', $fs_group_ids) . ')...'); 616 | 617 | $this->wikiMember->removeGroup($wiki_group_name); 618 | $madeChange = true; 619 | } 620 | } 621 | 622 | // No need to save if nothing has happened 623 | if ($madeChange) 624 | { 625 | $this->MWlogger->debug('Saved wiki group changes...'); 626 | 627 | $this->setUserOption('forum_last_update_groups', time()); 628 | $this->wikiMember->saveSettings(); 629 | } 630 | } 631 | 632 | /** 633 | * Create our user in MediaWiki. 634 | * This also sets a security check to attempt to prevent account takeovers by later on 635 | * checking the ids match prior to authorizing the user. 636 | * 637 | * @return void No return is expected. 638 | */ 639 | private function createWikiUser(): void 640 | { 641 | $this->MWlogger->debug('User does not exist in wiki, creating user...'); 642 | 643 | $this->wikiMember->setName($this->ForumMemberNameCleaned); 644 | $this->wikiMember->setEmail($this->fs->getMemberEmailAddress($this->ForumMember)); 645 | $this->wikiMember->setRealName($this->fs->getMemberRealName($this->ForumMember)); 646 | $this->wikiMember->mEmailAuthenticated = wfTimestampNow(); 647 | 648 | $this->wikiMember->addToDatabase(); 649 | 650 | // This is so we can validate which wiki members are attributed to which forum members. 651 | // Could be used used in the future to prevent account takeovers due to account renames. 652 | $this->setUserOption('forum_member_id', $this->fs->getMemberID($this->ForumMember)); 653 | 654 | $this->setUserOption('forum_last_update', time()); 655 | $this->wikiMember->saveSettings(); 656 | } 657 | 658 | /** 659 | * Checks if a member is banned on the forum software if enabled. 660 | * As this may be extensive or resource intensive, this check is cached. 661 | * This is handed off to the forum software provider. 662 | * 663 | * @return bool True if they are banned, false if they are not. 664 | */ 665 | private function memberIsBannedOnForum(): bool 666 | { 667 | // Disbled ban check? 668 | if (empty($this->ForumSettings['EnableBanCheck'])) 669 | return false; 670 | 671 | $this->MWlogger->debug('Checking ban status.'); 672 | 673 | // Check their ban once every 5 minutes. 674 | if (!(time() > ((int) $this->getUserOption('forum_last_update_banx') + $this->banned_check_interval))) 675 | { 676 | $this->MWlogger->debug('Cached banned status is {BAN}', array('BAN' => $this->getUserOption('forum_last_update_ban') !== 0 ? 'NOT banned' : 'banned')); 677 | return $this->getUserOption('forum_is_banned', 'bool'); 678 | } 679 | 680 | // Ask the forum if this member is banned. 681 | $banned = $this->fs->checkBans($this->ForumMember); 682 | 683 | $this->MWlogger->debug('Ban check completed, User is {BAN}', array('BAN' => $banned !== 0 ? 'NOT banned' : 'banned')); 684 | 685 | // Cache this for future hits. 686 | $this->setUserOption('forum_last_update_ban', time()); 687 | $this->setUserOption('forum_is_banned', $banned, 'boo'); 688 | 689 | return $banned; 690 | } 691 | 692 | /** 693 | * Wraps up using the MediaWiki Options to get user options. 694 | * It is deprecated to use the methods under the MediaWiki User Instance. 695 | * This will create a object handler if needed. This attempts to use proper methods based off the input type. 696 | * 697 | * @param string $option_name The name of the option we are attempting to load. 698 | * @param string $type The type the option is. Either string|s, bool|b or int|i (default). 699 | * @return mixed If we don't have a member, we return null, otherwise we pass the return to the MediaWiki handler. 700 | */ 701 | private function getUserOption(string $option_name, string $type = 'int', $default = 0) 702 | { 703 | if (empty($this->wikiMember) || !is_object($this->wikiMember)) 704 | { 705 | $this->MWlogger->debug('Attempted to call getUserOption prior to User Instance existing'); 706 | 707 | return null; 708 | } 709 | 710 | if (empty($this->wikiMemberOptions) || !is_object($this->wikiMemberOptions)) 711 | $this->wikiMemberOptions = \MediaWiki\MediaWikiServices::getInstance()->getUserOptionsManager(); 712 | 713 | if ($type === 'string' || $type === 's') 714 | return $this->wikiMemberOptions->getOption($this->wikiMember, $option_name, $default); 715 | if ($type === 'bool' || $type === 'b') 716 | return $this->wikiMemberOptions->getBoolOption($this->wikiMember, $option_name, $default); 717 | else 718 | return $this->wikiMemberOptions->getIntOption($this->wikiMember, $option_name, $default); 719 | } 720 | 721 | /** 722 | * Wraps up using the MediaWiki Options to set user options. 723 | * It is deprecated to use the methods under the MediaWiki User Instance. 724 | * This will create a object handler if needed. This attempts to use proper methods based off the input type. 725 | * 726 | * @param string $option_name The name of the option we are attempting to load. 727 | * @param mixed $value The value we are setting 728 | * @param string $type The type the option is. Either string|s, bool|b or int|i (default). 729 | * @return mixed If we don't have a member, we return null, otherwise we pass the return to the MediaWiki handler. 730 | */ 731 | private function setUserOption(string $option_name, $value, string $type = 'int') 732 | { 733 | if (empty($this->wikiMember) || !is_object($this->wikiMember)) 734 | { 735 | $this->MWlogger->debug('Attempted to call setUserOption prior to User Instance existing'); 736 | 737 | return null; 738 | } 739 | 740 | if (empty($this->wikiMemberOptions) || !is_object($this->wikiMemberOptions)) 741 | $this->wikiMemberOptions = \MediaWiki\MediaWikiServices::getInstance()->getUserOptionsManager(); 742 | 743 | if ($type === 'string' || $type === 's') 744 | return $this->wikiMemberOptions->setOption($this->wikiMember, $option_name, (string) $value); 745 | elseif ($type === 'bool' || $type === 'b') 746 | return $this->wikiMemberOptions->setOption($this->wikiMember, $option_name, (bool) $value); 747 | else 748 | return $this->wikiMemberOptions->setOption($this->wikiMember, $option_name, (int) $value); 749 | } 750 | 751 | /** 752 | /** 753 | * Wraps up using the MediaWiki User Groups to fetch effective groups. 754 | * It is deprecated to use the methods under the MediaWiki User Instance. 755 | * This will create a object handler if needed. This attempts to use proper methods based off the input type. 756 | * 757 | * @param string $option_name The name of the option we are attempting to load. 758 | * @param mixed $value The value we are setting 759 | * @param string $type The type the option is. Either string|s, bool|b or int|i (default). 760 | * @return mixed If we don't have a member, we return null, otherwise we pass the return to the MediaWiki handler. 761 | */ 762 | private function getUserEffectiveGroups(bool $recache = false) 763 | { 764 | if (empty($this->wikiMember) || !is_object($this->wikiMember)) 765 | { 766 | $this->MWlogger->debug('Attempted to call setUserOption prior to User Instance existing'); 767 | 768 | return null; 769 | } 770 | 771 | if (empty($this->wikiMemberGroups) || !is_object($this->wikiMemberGroups)) 772 | $this->wikiMemberGroups = \MediaWiki\MediaWikiServices::getInstance()->getUserGroupManager(); 773 | 774 | return $this->wikiMemberGroups->getUserEffectiveGroups( 775 | $this->wikiMember, 776 | 0 /* protected $queryFlagsUsed = self::READ_NORMAL; public const READ_NORMAL = 0;*/, 777 | $recache 778 | ); 779 | } 780 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ForumSessionProvider License 2 | ============================ 3 | Copyright (c) 2020-2022, Simple Machines 4 | Copyright (c) 2019, SleePy 5 | Copyright (c) 2019, Vekseid 6 | 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This MediaWiki extension allows users in an [Elkarte Forum](https://www.elkarte.net/) or [SMF forum](https://www.simplemachines.org/) to be automatically signed in if they are of the appropriate user group while logged into the forum. 2 | 3 | --- 4 | # Branches 5 | | Branch | MediaWiki | Elkarte | SMF | 6 | | ------ | --------- | ------- | ------- | 7 | | master | 1.43+ | 1.0,1.1 | 2.0,2.1 | 8 | | mw139 | 1.39 | 1.0,1.1 | 2.0,2.1 | 9 | | mw135 | 1.35 | 1.0,1.1 | 2.0,2.1 | 10 | 11 | 12 | # Install 13 | ## Manual 14 | Download the repository as a zip extract to extensions/ForumSsoProvider 15 | 16 | ## Git 17 | If your MediaWiki install follows your internal git repository, you can add this as a sub-module 18 | ``` 19 | # git submodule add https://github.com/SimpleMachines/smf-mw-auth.git extensions/ForumSsoProvider 20 | ``` 21 | 22 | ## Composer 23 | We are listed on [packagist](https://packagist.org/packages/simplemachines/forum-sso-provider) and can be installed using composer. Any changes you make to files here may be lost during updates. 24 | 25 | ``` 26 | COMPOSER=composer.local.json composer require --no-update SimpleMachines/Forum-Sso-Provider:dev-master 27 | composer update --no-dev 28 | ``` 29 | You can also uninstall at any time 30 | ``` 31 | COMPOSER=composer.local.json composer remove --no-update SimpleMachines/Forum-Sso-Provider 32 | composer update --no-dev 33 | ``` 34 | 35 | # Configuration 36 | To use, the contents of the ForumSsoProvider directory need to be placed into extensions/ForumSsoProvider. It is then loaded using the 'new' plugin loading method in LocalSettings.php: 37 | 38 | wfLoadExtension('ForumSsoProvider'); 39 | 40 | # Required LocalSettings 41 | All settings should be defined prior to calling `wfLoadExtension('ForumSsoProvider');` in your LocalSettings.php 42 | ### Path to Forum Software 43 | 44 | $wgFSPPath = '/path/to/smf/root/'; 45 | 46 | ### Forum software. Supports smf2.0, smf2.1, elk1.0, elk1.1 47 | 48 | $wgFSPSoftware = 'smf2.1'; 49 | 50 | # Optional LocalSettings 51 | ### Login Groups - Users in this group are signed into MediaWiki. SMF does not have a real group for "Regular" members; it is a pseudo group. Additionally, Local Moderators (Group ID 3) are a special group. Users are only in this group when they browse the board they were granted moderator permissions. If you do not specify this, users are granted permission to the wiki by default. If you specify this, users must be in the associated groups to have access to the wiki. 52 | 53 | $wgFSPAllowGroups = array(5); 54 | 55 | ### Deny Groups - Prevent users in these groups from being signed into MediaWiki; this is a deny group and takes over the login group. 56 | 57 | $wgFSPDenyGroups = array(4); 58 | 59 | ### Admin Groups - Users in these groups are granted sysop access in MediaWiki. 60 | 61 | $wgFSPAdminGroups = array(1, 2); 62 | 63 | ### Super Groups - Users in these groups are granted bureaucrat access in MediaWiki. 64 | 65 | $wgFSPSuperGroups = array(1); 66 | 67 | ### Interface Groups - Users in these groups are granted interface-admin access in MediaWiki. 68 | 69 | $wgFSPInterfaceGroups = array(1); 70 | 71 | ### Special Groups - An key-valued array of {SMF Group ID} => {MediaWiki Group Name} 72 | 73 | $wgFSPSpecialGroups = array( 74 | 11 => 'Custom_Wiki_group', 75 | ); 76 | 77 | ### Ban checks - Enable checking against bans in SMF. If found, it prevents access to MediaWiki. 78 | 79 | $wgFSPEnableBanCheck = true; 80 | 81 | ### Lockdown permissions to prevent new account creations/modifications. 82 | 83 | $wgGroupPermissions['*']['createaccount'] = false; 84 | $wgGroupPermissions['*']['read'] = true; 85 | $wgGroupPermissions['*']['edit'] = false; 86 | $wgGroupPermissions['*']['createtalk'] = false; 87 | $wgGroupPermissions['*']['createpage'] = false; 88 | $wgGroupPermissions['*']['writeapi'] = false; 89 | $wgGroupPermissions['user']['move'] = true; 90 | $wgGroupPermissions['user']['read'] = true; 91 | $wgGroupPermissions['user']['edit'] = true; 92 | $wgGroupPermissions['user']['upload'] = true; 93 | $wgGroupPermissions['user']['autoconfirmed'] = true; 94 | $wgGroupPermissions['user']['emailconfirmed'] = true; 95 | $wgGroupPermissions['user']['createtalk'] = true; 96 | $wgGroupPermissions['user']['createpage'] = true; 97 | $wgGroupPermissions['user']['writeapi'] = true; 98 | 99 | # Legacy Settings 100 | These settings are used by the legacy Auth_SMF.php. 101 | ### Uses the legacy Auth_SMF.php LocalSettings 102 | 103 | define('SMF_IN_WIKI', true); 104 | $wgSMFLogin = true; 105 | 106 | ### Login Groups - Users in this group are signed into MediaWiki. 107 | 108 | $wgSMFGroupID = array(2); 109 | 110 | ### Deny Groups - Prevent users in these groups from being signed into MediaWiki; this is a deny group and takes over the login group. 111 | 112 | $wgSMFDenyGroupID = array(4); 113 | 114 | ### Admin Groups - Users in these groups are granted sysop access in MediaWiki. 115 | 116 | $wgSMFAdminGroupID = array(1, 2); 117 | 118 | ### Special Groups - An key-valued array of {SMF Group ID} => {MediaWiki Group Name} 119 | 120 | $wgSMFSpecialGroups = array( 121 | 11 => 'Custom_Wiki_group', 122 | ); 123 | 124 | ### Forum Software Cookie. 125 | 126 | $wgCookieDomain = 'domain.tld'; 127 | 128 | SMF Default Groups 129 | --------------- 130 | | Group ID | Group Name | Post Group | 131 | | ---- | --------- | --- | 132 | | 1 | Administrator | No | 133 | | 2 | Global Moderator | No | 134 | | 4 | Newbie | Yes | 135 | | 5 | Jr. Member | Yes | 136 | | 6 | Full Member | Yes | 137 | | 7 | Sr. Member | Yes | 138 | | 8 | Hero Member | Yes | 139 | 140 | Finding your SMF Group ID 141 | --------------- 142 | 1. Navigate to the Admin Control Panel 143 | 2. Click on Membergroups 144 | 3. Click Modify on the group you are looking for. 145 | 4. In the address bar, you will see `group=####`, this number is the group ID. 146 | 147 | Working with Arrays 148 | --------------- 149 | The configuration file uses basic PHP code. 150 | 151 | When you have a single member for the array, you can wrap it in the array statement. 152 | $code = array(1); 153 | 154 | If you have 2 members that need to go into the array, use a comma to separate them. 155 | $code = array(1,2); 156 | 157 | When you have strings, wrap them in quotes. SMF coding style recommends using single quotes unless necessary. Double quotes signal the PHP parser to use special handling, which might interpret variables and other logic inside the string. This is typically not necessary in the configuration. 158 | $code = array('my string'); 159 | 160 | You may also see the shorthand square brackets to refer to arrays. 161 | $code = ['my string']; 162 | 163 | Extension Troubleshooting 164 | --------------- 165 | 166 | 1. Set $wgDebugLogFile in your `LocalSettings.php`: 167 | 168 | $wgDebugLogFile = "/some/private/path/MediaWiki.log"; 169 | 170 | 2. Trigger the URL either yourself or the member you are debugging. 171 | 172 | 3. Search the file for ForumSessionProvider. 173 | 174 | 4. The messages will indicate how it is processing the authorization. 175 | 176 | This grows quickly and uses up storage space. You can delete the file, and it will be recreated again. 177 | 178 | Wiki Troubleshooting 179 | --------------- 180 | MediaWiki has built-in methods for debugging it. If the extension is acting up and the debugging log is not providing information. Add the following to your `LocalSettings.php`. This should not be run in a production forum, as it may expose sensitive details 181 | 182 | $wgShowExceptionDetails = true; 183 | $wgShowSQLErrors = true; 184 | $wgDebugDumpSql = true; 185 | $wgShowDBErrorBacktrace = true; 186 | Remove when you are done debugging. 187 | 188 | ---- 189 | Getting New SMF Forks In 190 | ------------------------ 191 | If you know how your fork's authentication works, feel free to submit a pull request. 192 | 193 | Issues or changes 194 | ------------------------ 195 | If a bug has occurred, please open a new issue. If you have a change, please submit a pull request. 196 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simplemachines/forum-sso-provider", 3 | "type": "mediawiki-extension", 4 | "description": "An extension to MediaWiki that allows an Elkarte Forum or SMF forum to be automatically signed in if they are of the appropriate usergroup while logged into the forum.", 5 | "keywords": [ 6 | "SMF Mediawiki Auth", 7 | "Elkare Mediawiki Auth" 8 | ], 9 | "homepage": "https://github.com/SimpleMachines/smf-mw-auth/", 10 | "license": "BSD-3-Clause", 11 | "authors": [ 12 | { 13 | "name": "Vekseid", 14 | "role": "Original author" 15 | }, 16 | { 17 | "name": "Simple Machines", 18 | "role": "Developer" 19 | }, 20 | { 21 | "name": "jdarwood007", 22 | "role": "Core Developer" 23 | } 24 | ], 25 | "require": { 26 | "php": ">=8.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ForumSsoProvider", 3 | "version": "2.1.0", 4 | "author": [ 5 | "Simple Machines", 6 | "SleePy", 7 | "Vekseid" 8 | ], 9 | "url": "https://github.com/SimpleMachines/smf-mw-auth", 10 | "description": "Users a Forum Software to provide MediaWiki single-sign on.", 11 | "license-name": "BSD", 12 | "type": "other", 13 | "requires": { 14 | "MediaWiki": ">= 1.38.0" 15 | }, 16 | "config": { 17 | "_prefix": "wgFSP", 18 | "Path": "", 19 | "NameStyle": "smf", 20 | "Software": "elk1.1", 21 | "SuperGroups": [], 22 | "InterfaceGroups": [1], 23 | "AdminGroups": [1], 24 | "AllowGroups": [], 25 | "DenyGroups:": [] 26 | }, 27 | "SessionProviders": { 28 | "ForumSsoProvider": { 29 | "class": "ForumSsoProvider", 30 | "args": [] 31 | } 32 | }, 33 | "AuthManagerAutoConfig": { 34 | "primaryauth": { 35 | "ForumAuthManager": { 36 | "class": "ForumAuthManager", 37 | "args": [] 38 | } 39 | } 40 | }, 41 | "DefaultUserOptions": { 42 | "forum_last_update": 0 43 | }, 44 | "Hooks": { 45 | "SpecialPageBeforeExecute": [ 46 | "ForumSsoProvider::onSpecialPageBeforeExecute" 47 | ] 48 | }, 49 | "AutoloadClasses": { 50 | "ForumSsoProvider": "ForumSsoProvider.php", 51 | "ForumAuthManager": "ForumAuthManager.php", 52 | "ForumDatabaseProvider": "DatabaseProvider/base.php", 53 | "ForumDatabaseProviderMySQLi": "DatabaseProvider/MySQLi.php", 54 | "ForumDatabaseProviderPDO": "DatabaseProvider/PDO.php", 55 | "ForumSoftwareProvider": "ForumProvider/base.php", 56 | "ForumSoftwareProvidersmf20": "ForumProvider/smf2.0.php", 57 | "ForumSoftwareProvidersmf21": "ForumProvider/smf2.1.php", 58 | "ForumSoftwareProviderelk10": "ForumProvider/elk1.0.php", 59 | "ForumSoftwareProviderelk11": "ForumProvider/elk1.1.php" 60 | 61 | }, 62 | "manifest_version": 1 63 | } --------------------------------------------------------------------------------