openid.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. <?php
  2. // This file is part of GNU social - https://www.gnu.org/software/social
  3. //
  4. // GNU social is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // GNU social is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * @copyright 2008, 2009 StatusNet, Inc.
  18. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  19. */
  20. defined('GNUSOCIAL') || die();
  21. require_once('Auth/OpenID.php');
  22. require_once('Auth/OpenID/Consumer.php');
  23. require_once('Auth/OpenID/Server.php');
  24. require_once('Auth/OpenID/SReg.php');
  25. require_once('Auth/OpenID/MySQLStore.php');
  26. // About one year cookie expiry
  27. define('OPENID_COOKIE_EXPIRY', round(365.25 * 24 * 60 * 60));
  28. define('OPENID_COOKIE_KEY', 'lastusedopenid');
  29. function oid_store()
  30. {
  31. static $store = null;
  32. if (is_null($store)) {
  33. // To create a new Database connection is an absolute must
  34. // because database is in transaction (auto-commit = false)
  35. // mode during OpenID operation
  36. // Is a must because our Internal Session Handler uses database
  37. // and depends on auto-commit = true
  38. $dsn = common_config('db', 'database');
  39. $options = PEAR::getStaticProperty('DB', 'options');
  40. if (!is_array($options)) {
  41. $options = [];
  42. }
  43. $db = DB::connect($dsn, $options);
  44. if ((new PEAR)->isError($db)) {
  45. throw new ServerException($db->getMessage());
  46. }
  47. switch (common_config('db', 'type')) {
  48. case 'pgsql':
  49. $store = new Auth_OpenID_PostgreSQLStore($db);
  50. break;
  51. case 'mysql':
  52. $store = new Auth_OpenID_MySQLStore($db);
  53. break;
  54. default:
  55. throw new ServerException('Unknown DB type selected.');
  56. }
  57. }
  58. return $store;
  59. }
  60. function oid_consumer()
  61. {
  62. $store = oid_store();
  63. // No need to declare a Yadis Session Handler
  64. common_ensure_session(); // This is transparent to OpenID's eyes
  65. $consumer = new Auth_OpenID_Consumer($store);
  66. return $consumer;
  67. }
  68. function oid_server()
  69. {
  70. $store = oid_store();
  71. $server = new Auth_OpenID_Server($store, common_local_url('openidserver'));
  72. return $server;
  73. }
  74. function oid_clear_last()
  75. {
  76. oid_set_last('');
  77. }
  78. function oid_set_last($openid_url)
  79. {
  80. common_set_cookie(
  81. OPENID_COOKIE_KEY,
  82. $openid_url,
  83. time() + OPENID_COOKIE_EXPIRY
  84. );
  85. }
  86. function oid_get_last()
  87. {
  88. if (empty($_COOKIE[OPENID_COOKIE_KEY])) {
  89. return null;
  90. }
  91. $openid_url = $_COOKIE[OPENID_COOKIE_KEY];
  92. if ($openid_url && strlen($openid_url) > 0) {
  93. return $openid_url;
  94. } else {
  95. return null;
  96. }
  97. }
  98. function oid_link_user($id, $canonical, $display)
  99. {
  100. global $_PEAR;
  101. $oid = new User_openid();
  102. $oid->user_id = $id;
  103. $oid->canonical = $canonical;
  104. $oid->display = $display;
  105. $oid->created = common_sql_now();
  106. if (!$oid->insert()) {
  107. $err = &$_PEAR->getStaticProperty('DB_DataObject', 'lastError');
  108. return false;
  109. }
  110. return true;
  111. }
  112. function oid_get_user($openid_url)
  113. {
  114. $user = null;
  115. $oid = User_openid::getKV('canonical', $openid_url);
  116. if ($oid) {
  117. $user = User::getKV('id', $oid->user_id);
  118. }
  119. return $user;
  120. }
  121. function oid_check_immediate($openid_url, $backto=null)
  122. {
  123. if (!$backto) {
  124. $action = $_REQUEST['action'];
  125. $args = common_copy_args($_GET);
  126. unset($args['action']);
  127. $backto = common_local_url($action, $args);
  128. }
  129. common_ensure_session();
  130. $_SESSION['openid_immediate_backto'] = $backto;
  131. oid_authenticate(
  132. $openid_url,
  133. 'finishimmediate',
  134. true
  135. );
  136. }
  137. function oid_authenticate($openid_url, $returnto, $immediate=false)
  138. {
  139. $openid_url = Auth_OpenID::normalizeUrl($openid_url);
  140. if (!common_valid_http_url($openid_url)) {
  141. throw new ClientException(_m('No valid URL provided for OpenID.'));
  142. }
  143. $consumer = oid_consumer();
  144. if (!$consumer) {
  145. // TRANS: OpenID plugin server error.
  146. throw new ServerException(_m('Cannot instantiate OpenID consumer object.'));
  147. }
  148. common_ensure_session();
  149. $auth_request = $consumer->begin($openid_url);
  150. // Handle failure status return values.
  151. if (!$auth_request) {
  152. common_log(LOG_ERR, __METHOD__ . ": mystery fail contacting $openid_url");
  153. // TRANS: OpenID plugin message. Given when an OpenID is not valid.
  154. throw new ServerException(_m('Not a valid OpenID.'));
  155. } elseif (Auth_OpenID::isFailure($auth_request)) {
  156. common_log(LOG_ERR, __METHOD__ . ": OpenID fail to $openid_url: $auth_request->message");
  157. // TRANS: OpenID plugin server error. Given when the OpenID authentication request fails.
  158. // TRANS: %s is the failure message.
  159. throw new ServerException(sprintf(_m('OpenID failure: %s.'), $auth_request->message));
  160. }
  161. $sreg_request = Auth_OpenID_SRegRequest::build(
  162. // Required
  163. [],
  164. // Optional
  165. [
  166. 'nickname',
  167. 'email',
  168. 'fullname',
  169. 'language',
  170. 'timezone',
  171. 'postcode',
  172. 'country',
  173. ]
  174. );
  175. if ($sreg_request) {
  176. $auth_request->addExtension($sreg_request);
  177. }
  178. $requiredTeam = common_config('openid', 'required_team');
  179. if ($requiredTeam) {
  180. // LaunchPad OpenID extension
  181. $team_request = new Auth_OpenID_TeamsRequest(array($requiredTeam));
  182. if ($team_request) {
  183. $auth_request->addExtension($team_request);
  184. }
  185. }
  186. $trust_root = common_root_url(true);
  187. $process_url = common_local_url($returnto);
  188. // Net::OpenID::Server as used on LiveJournal appears to incorrectly
  189. // reject POST requests for data submissions that OpenID 1.1 specs
  190. // as GET, although 2.0 allows them:
  191. // https://rt.cpan.org/Public/Bug/Display.html?id=42202
  192. //
  193. // Our OpenID libraries would have switched in the redirect automatically
  194. // if it were detecting 1.1 compatibility mode, however the server is
  195. // advertising itself as 2.0-compatible, so we got switched to the POST.
  196. //
  197. // Since the GET should always work anyway, we'll just take out the
  198. // autosubmitter for now.
  199. //
  200. //if ($auth_request->shouldSendRedirect()) {
  201. $redirect_url = $auth_request->redirectURL(
  202. $trust_root,
  203. $process_url,
  204. $immediate
  205. );
  206. if (Auth_OpenID::isFailure($redirect_url)) {
  207. // TRANS: OpenID plugin server error. Given when the OpenID authentication request cannot be redirected.
  208. // TRANS: %s is the failure message.
  209. throw new ServerException(sprintf(_m('Could not redirect to server: %s.'), $redirect_url->message));
  210. }
  211. common_redirect($redirect_url, 303);
  212. /*
  213. } else {
  214. // Generate form markup and render it.
  215. $form_id = 'openid_message';
  216. $form_html = $auth_request->formMarkup($trust_root, $process_url,
  217. $immediate, array('id' => $form_id));
  218. // XXX: This is cheap, but things choke if we don't escape ampersands
  219. // in the HTML attributes
  220. $form_html = preg_replace('/&/', '&amp;', $form_html);
  221. // Display an error if the form markup couldn't be generated;
  222. // otherwise, render the HTML.
  223. if (Auth_OpenID::isFailure($form_html)) {
  224. // TRANS: OpenID plugin server error if the form markup could not be generated.
  225. // TRANS: %s is the failure message.
  226. common_server_error(sprintf(_m('Could not create OpenID form: %s'), $form_html->message));
  227. } else {
  228. $action = new AutosubmitAction(); // see below
  229. $action->form_html = $form_html;
  230. $action->form_id = $form_id;
  231. $action->prepare(array('action' => 'autosubmit'));
  232. $action->handle(array('action' => 'autosubmit'));
  233. }
  234. }
  235. */
  236. }
  237. // Half-assed attempt at a Plugin-private function
  238. function _oid_print_instructions()
  239. {
  240. common_element(
  241. 'div',
  242. 'instructions',
  243. // TRANS: OpenID plugin user instructions.
  244. _m('This form should automatically submit itself. '.
  245. 'If not, click the submit button to go to your '.
  246. 'OpenID provider.')
  247. );
  248. }
  249. /**
  250. * Update a user from sreg parameters
  251. * @param User $user
  252. * @param array $sreg fields from OpenID sreg response
  253. * @access private
  254. */
  255. function oid_update_user($user, $sreg)
  256. {
  257. $profile = $user->getProfile();
  258. $orig_profile = clone($profile);
  259. if (!empty($sreg['fullname']) && strlen($sreg['fullname']) <= 255) {
  260. $profile->fullname = $sreg['fullname'];
  261. }
  262. if (!empty($sreg['country'])) {
  263. if ($sreg['postcode']) {
  264. // XXX: use postcode to get city and region
  265. // XXX: also, store postcode somewhere -- it's valuable!
  266. $profile->location = $sreg['postcode'] . ', ' . $sreg['country'];
  267. } else {
  268. $profile->location = $sreg['country'];
  269. }
  270. }
  271. // XXX save language if it's passed
  272. // XXX save timezone if it's passed
  273. if (!$profile->update($orig_profile)) {
  274. // TRANS: OpenID plugin server error.
  275. common_server_error(_m('Error saving the profile.'));
  276. return false;
  277. }
  278. $orig_user = clone($user);
  279. if (!empty($sreg['email']) && Validate::email($sreg['email'], common_config('email', 'check_domain'))) {
  280. $user->email = $sreg['email'];
  281. }
  282. if (!$user->update($orig_user)) {
  283. // TRANS: OpenID plugin server error.
  284. common_server_error(_m('Error saving the user.'));
  285. return false;
  286. }
  287. return true;
  288. }
  289. function oid_assert_allowed($url)
  290. {
  291. $blacklist = common_config('openid', 'blacklist');
  292. $whitelist = common_config('openid', 'whitelist');
  293. if (empty($blacklist)) {
  294. $blacklist = array();
  295. }
  296. if (empty($whitelist)) {
  297. $whitelist = array();
  298. }
  299. foreach ($blacklist as $pattern) {
  300. if (preg_match("/$pattern/", $url)) {
  301. common_log(LOG_INFO, "Matched OpenID blacklist pattern {$pattern} with {$url}");
  302. foreach ($whitelist as $exception) {
  303. if (preg_match("/$exception/", $url)) {
  304. common_log(LOG_INFO, "Matched OpenID whitelist pattern {$exception} with {$url}");
  305. return;
  306. }
  307. }
  308. // TRANS: OpenID plugin client exception (403).
  309. throw new ClientException(_m('Unauthorized URL used for OpenID login.'), 403);
  310. }
  311. }
  312. return;
  313. }
  314. /**
  315. * Check the teams available in the given OpenID response
  316. * Using Launchpad's OpenID teams extension
  317. *
  318. * @return boolean whether this user is acceptable
  319. */
  320. function oid_check_teams($response)
  321. {
  322. $requiredTeam = common_config('openid', 'required_team');
  323. if ($requiredTeam) {
  324. $team_resp = new Auth_OpenID_TeamsResponse($response);
  325. if ($team_resp) {
  326. $teams = $team_resp->getTeams();
  327. } else {
  328. $teams = array();
  329. }
  330. $match = in_array($requiredTeam, $teams);
  331. $is = $match ? 'is' : 'is not';
  332. common_log(LOG_DEBUG, "Remote user $is in required team $requiredTeam: [" . implode(', ', $teams) . "]");
  333. return $match;
  334. }
  335. return true;
  336. }
  337. class AutosubmitAction extends Action
  338. {
  339. public $form_html = null;
  340. public $form_id = null;
  341. public function handle()
  342. {
  343. parent::handle();
  344. $this->showPage();
  345. }
  346. public function title()
  347. {
  348. // TRANS: Title
  349. return _m('OpenID Login Submission');
  350. }
  351. public function showContent()
  352. {
  353. $this->raw('<p style="margin: 20px 80px">');
  354. // @todo FIXME: This would be better using standard CSS class, but the present theme's a bit scary.
  355. $this->element('img', array('src' => Theme::path('images/icons/icon_processing.gif', 'base'),
  356. // for some reason the base CSS sets <img>s as block display?!
  357. 'style' => 'display: inline'));
  358. // TRANS: OpenID plugin message used while requesting authorization user's OpenID login provider.
  359. $this->text(_m('Requesting authorization from your login provider...'));
  360. $this->raw('</p>');
  361. $this->raw('<p style="margin-top: 60px; font-style: italic">');
  362. // TRANS: OpenID plugin message. User instruction while requesting authorization user's OpenID login provider.
  363. $this->text(_m('If you are not redirected to your login provider in a few seconds, try pushing the button below.'));
  364. $this->raw('</p>');
  365. $this->raw($this->form_html);
  366. }
  367. public function showScripts()
  368. {
  369. parent::showScripts();
  370. $this->element(
  371. 'script',
  372. null,
  373. 'document.getElementById(\'' . $this->form_id . '\').submit();'
  374. );
  375. }
  376. }