openid.php 13 KB

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