You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

689 lines
20 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
8 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. <?php
  2. use Abraham\TwitterOAuth\TwitterOAuth;
  3. function require_login(&$app, $redirect=true) {
  4. $params = $app->request()->params();
  5. if(array_key_exists('token', $params)) {
  6. try {
  7. $data = JWT::decode($params['token'], Config::$jwtSecret, array('HS256'));
  8. $_SESSION['user_id'] = $data->user_id;
  9. $_SESSION['me'] = $data->me;
  10. } catch(DomainException $e) {
  11. if($redirect) {
  12. header('X-Error: DomainException');
  13. $app->redirect('/', 302);
  14. } else {
  15. return false;
  16. }
  17. } catch(UnexpectedValueException $e) {
  18. if($redirect) {
  19. header('X-Error: UnexpectedValueException');
  20. $app->redirect('/', 302);
  21. } else {
  22. return false;
  23. }
  24. }
  25. }
  26. if(!array_key_exists('user_id', $_SESSION)) {
  27. if($redirect)
  28. $app->redirect('/', 302);
  29. return false;
  30. } else {
  31. return ORM::for_table('users')->find_one($_SESSION['user_id']);
  32. }
  33. }
  34. function generate_login_token($opts=[]) {
  35. return JWT::encode(array_merge([
  36. 'user_id' => $_SESSION['user_id'],
  37. 'me' => $_SESSION['me'],
  38. 'created_at' => time()
  39. ], $opts), Config::$jwtSecret);
  40. }
  41. $app->get('/dashboard', function() use($app) {
  42. if($user=require_login($app)) {
  43. render('dashboard', array(
  44. 'title' => 'Dashboard',
  45. 'authorizing' => false
  46. ));
  47. }
  48. });
  49. $app->get('/new', function() use($app) {
  50. if($user=require_login($app)) {
  51. $params = $app->request()->params();
  52. $entry = false;
  53. $in_reply_to = '';
  54. if(array_key_exists('reply', $params))
  55. $in_reply_to = $params['reply'];
  56. $test_response = '';
  57. if($user->last_micropub_response) {
  58. try {
  59. if(@json_decode($user->last_micropub_response)) {
  60. $d = json_decode($user->last_micropub_response);
  61. $test_response = $d->response;
  62. }
  63. } catch(Exception $e) {
  64. }
  65. }
  66. render('new-post', array(
  67. 'title' => 'New Post',
  68. 'in_reply_to' => $in_reply_to,
  69. 'micropub_endpoint' => $user->micropub_endpoint,
  70. 'media_endpoint' => $user->micropub_media_endpoint,
  71. 'micropub_scope' => $user->micropub_scope,
  72. 'micropub_access_token' => $user->micropub_access_token,
  73. 'response_date' => $user->last_micropub_response_date,
  74. 'syndication_targets' => json_decode($user->syndication_targets, true),
  75. 'test_response' => $test_response,
  76. 'location_enabled' => $user->location_enabled,
  77. 'user' => $user,
  78. 'authorizing' => false
  79. ));
  80. }
  81. });
  82. $app->get('/bookmark', function() use($app) {
  83. if($user=require_login($app)) {
  84. $params = $app->request()->params();
  85. $url = '';
  86. $name = '';
  87. $content = '';
  88. $tags = '';
  89. if(array_key_exists('url', $params))
  90. $url = $params['url'];
  91. if(array_key_exists('name', $params))
  92. $name = $params['name'];
  93. if(array_key_exists('content', $params))
  94. $content = $params['content'];
  95. render('new-bookmark', array(
  96. 'title' => 'New Bookmark',
  97. 'bookmark_url' => $url,
  98. 'bookmark_name' => $name,
  99. 'bookmark_content' => $content,
  100. 'bookmark_tags' => $tags,
  101. 'token' => generate_login_token(),
  102. 'syndication_targets' => json_decode($user->syndication_targets, true),
  103. 'user' => $user,
  104. 'authorizing' => false
  105. ));
  106. }
  107. });
  108. $app->get('/favorite', function() use($app) {
  109. if($user=require_login($app)) {
  110. $params = $app->request()->params();
  111. $like_of = '';
  112. if(array_key_exists('url', $params))
  113. $like_of = $params['url'];
  114. // Check if there was a login token in the query string and whether it has autosubmit=true
  115. $autosubmit = false;
  116. if(array_key_exists('token', $params)) {
  117. try {
  118. $data = JWT::decode($params['token'], Config::$jwtSecret, ['HS256']);
  119. if(isset($data->autosubmit) && $data->autosubmit) {
  120. // Only allow this token to be used for the user who created it
  121. if($data->user_id == $_SESSION['user_id']) {
  122. $autosubmit = true;
  123. }
  124. }
  125. } catch(Exception $e) {
  126. }
  127. }
  128. if(array_key_exists('edit', $params)) {
  129. $edit_data = get_micropub_source($user, $params['edit'], 'like-of');
  130. $url = $params['edit'];
  131. if(isset($edit_data['like-of'])) {
  132. $like_of = $edit_data['like-of'][0];
  133. }
  134. } else {
  135. $edit_data = false;
  136. $url = false;
  137. }
  138. render('new-favorite', array(
  139. 'title' => 'New Favorite',
  140. 'like_of' => $like_of,
  141. 'token' => generate_login_token(['autosubmit'=>true]),
  142. 'authorizing' => false,
  143. 'autosubmit' => $autosubmit,
  144. 'url' => $url
  145. ));
  146. }
  147. });
  148. $app->get('/event', function() use($app) {
  149. if($user=require_login($app)) {
  150. $params = $app->request()->params();
  151. render('event', array(
  152. 'title' => 'Event',
  153. 'authorizing' => false
  154. ));
  155. }
  156. });
  157. $app->get('/itinerary', function() use($app) {
  158. if($user=require_login($app)) {
  159. $params = $app->request()->params();
  160. render('new-itinerary', array(
  161. 'title' => 'Itinerary',
  162. 'authorizing' => false
  163. ));
  164. }
  165. });
  166. $app->get('/photo', function() use($app) {
  167. if($user=require_login($app)) {
  168. $params = $app->request()->params();
  169. render('photo', array(
  170. 'title' => 'New Photo',
  171. 'note_content' => '',
  172. 'authorizing' => false
  173. ));
  174. }
  175. });
  176. $app->get('/review', function() use($app) {
  177. if($user=require_login($app)) {
  178. $params = $app->request()->params();
  179. render('review', array(
  180. 'title' => 'Review',
  181. 'authorizing' => false
  182. ));
  183. }
  184. });
  185. $app->get('/repost', function() use($app) {
  186. if($user=require_login($app)) {
  187. $params = $app->request()->params();
  188. $repost_of = '';
  189. if(array_key_exists('url', $params))
  190. $repost_of = $params['url'];
  191. if(array_key_exists('edit', $params)) {
  192. $edit_data = get_micropub_source($user, $params['edit'], 'repost-of');
  193. $url = $params['edit'];
  194. if(isset($edit_data['repost-of'])) {
  195. $repost_of = $edit_data['repost-of'][0];
  196. }
  197. } else {
  198. $edit_data = false;
  199. $url = false;
  200. }
  201. render('new-repost', array(
  202. 'title' => 'New Repost',
  203. 'repost_of' => $repost_of,
  204. 'token' => generate_login_token(),
  205. 'authorizing' => false,
  206. 'url' => $url,
  207. ));
  208. }
  209. });
  210. $app->post('/prefs', function() use($app) {
  211. if($user=require_login($app)) {
  212. $params = $app->request()->params();
  213. $user->location_enabled = $params['enabled'];
  214. $user->save();
  215. }
  216. $app->response()['Content-type'] = 'application/json';
  217. $app->response()->body(json_encode(array(
  218. 'result' => 'ok'
  219. )));
  220. });
  221. $app->post('/prefs/timezone', function() use($app) {
  222. // Called when the interface finds the user's location.
  223. // Look up the timezone for this location and store it as their default.
  224. $timezone = false;
  225. if($user=require_login($app)) {
  226. $params = $app->request()->params();
  227. $timezone = p3k\Timezone::timezone_for_location($params['latitude'], $params['longitude']);
  228. if($timezone) {
  229. $user->default_timezone = $timezone;
  230. $user->save();
  231. }
  232. }
  233. $app->response()['Content-type'] = 'application/json';
  234. $app->response()->body(json_encode(array(
  235. 'result' => 'ok',
  236. 'timezone' => $timezone,
  237. )));
  238. });
  239. $app->get('/add-to-home', function() use($app) {
  240. $params = $app->request()->params();
  241. header("Cache-Control: no-cache, must-revalidate");
  242. if(array_key_exists('token', $params) && !session('add-to-home-started')) {
  243. unset($_SESSION['add-to-home-started']);
  244. // Verify the token and sign the user in
  245. try {
  246. $data = JWT::decode($params['token'], Config::$jwtSecret, array('HS256'));
  247. $_SESSION['user_id'] = $data->user_id;
  248. $_SESSION['me'] = $data->me;
  249. $app->redirect('/new', 302);
  250. } catch(DomainException $e) {
  251. header('X-Error: DomainException');
  252. $app->redirect('/', 302);
  253. } catch(UnexpectedValueException $e) {
  254. header('X-Error: UnexpectedValueException');
  255. $app->redirect('/', 302);
  256. }
  257. } else {
  258. if($user=require_login($app)) {
  259. if(array_key_exists('start', $params)) {
  260. $_SESSION['add-to-home-started'] = true;
  261. $token = JWT::encode(array(
  262. 'user_id' => $_SESSION['user_id'],
  263. 'me' => $_SESSION['me'],
  264. 'created_at' => time()
  265. ), Config::$jwtSecret);
  266. $app->redirect('/add-to-home?token='.$token, 302);
  267. } else {
  268. unset($_SESSION['add-to-home-started']);
  269. render('add-to-home', array('title' => 'Quill'));
  270. }
  271. }
  272. }
  273. });
  274. $app->get('/email', function() use($app) {
  275. if($user=require_login($app)) {
  276. $test_response = '';
  277. if($user->last_micropub_response) {
  278. try {
  279. if(@json_decode($user->last_micropub_response)) {
  280. $d = json_decode($user->last_micropub_response);
  281. $test_response = $d->response;
  282. }
  283. } catch(Exception $e) {
  284. }
  285. }
  286. if(!$user->email_username) {
  287. $host = parse_url($user->url, PHP_URL_HOST);
  288. $user->email_username = $host . '.' . rand(100000,999999);
  289. $user->save();
  290. }
  291. render('email', array(
  292. 'title' => 'Post-by-Email',
  293. 'micropub_endpoint' => $user->micropub_endpoint,
  294. 'test_response' => $test_response,
  295. 'user' => $user
  296. ));
  297. }
  298. });
  299. $app->get('/settings', function() use($app) {
  300. if($user=require_login($app)) {
  301. render('settings', [
  302. 'title' => 'Settings',
  303. 'user' => $user,
  304. 'authorizing' => false
  305. ]);
  306. }
  307. });
  308. $app->post('/settings/save', function() use($app) {
  309. if($user=require_login($app)) {
  310. $params = $app->request()->params();
  311. if(array_key_exists('html_content', $params))
  312. $user->micropub_optin_html_content = $params['html_content'] ? 1 : 0;
  313. if(array_key_exists('slug_field', $params) && $params['slug_field'])
  314. $user->micropub_slug_field = $params['slug_field'];
  315. if(array_key_exists('syndicate_field', $params) && $params['syndicate_field']) {
  316. if(in_array($params['syndicate_field'], ['syndicate-to','mp-syndicate-to']))
  317. $user->micropub_syndicate_field = $params['syndicate_field'];
  318. }
  319. $user->save();
  320. $app->response()['Content-type'] = 'application/json';
  321. $app->response()->body(json_encode(array(
  322. 'result' => 'ok'
  323. )));
  324. }
  325. });
  326. $app->post('/settings/html-content', function() use($app) {
  327. if($user=require_login($app)) {
  328. $params = $app->request()->params();
  329. $user->micropub_optin_html_content = $params['html'] ? 1 : 0;
  330. $user->save();
  331. $app->response()['Content-type'] = 'application/json';
  332. $app->response()->body(json_encode(array(
  333. 'html' => $user->micropub_optin_html_content
  334. )));
  335. }
  336. });
  337. $app->get('/settings/html-content', function() use($app) {
  338. if($user=require_login($app)) {
  339. $app->response()['Content-type'] = 'application/json';
  340. $app->response()->body(json_encode(array(
  341. 'html' => $user->micropub_optin_html_content
  342. )));
  343. }
  344. });
  345. function create_favorite(&$user, $url) {
  346. $micropub_request = array(
  347. 'like-of' => $url
  348. );
  349. $r = micropub_post_for_user($user, $micropub_request);
  350. $tweet_id = false;
  351. // POSSE favorites to Twitter
  352. if($user->twitter_access_token && preg_match('/https?:\/\/(?:www\.)?twitter\.com\/[^\/]+\/status(?:es)?\/(\d+)/', $url, $match)) {
  353. $tweet_id = $match[1];
  354. $twitter = new TwitterOAuth(Config::$twitterClientID, Config::$twitterClientSecret,
  355. $user->twitter_access_token, $user->twitter_token_secret);
  356. $result = $twitter->post('favorites/create', array(
  357. 'id' => $tweet_id
  358. ));
  359. }
  360. return $r;
  361. }
  362. function edit_favorite(&$user, $post_url, $like_of) {
  363. $micropub_request = [
  364. 'action' => 'update',
  365. 'url' => $post_url,
  366. 'replace' => [
  367. 'like-of' => [$like_of]
  368. ]
  369. ];
  370. $r = micropub_post_for_user($user, $micropub_request, null, true);
  371. return $r;
  372. }
  373. function create_repost(&$user, $url) {
  374. $micropub_request = array(
  375. 'repost-of' => $url
  376. );
  377. $r = micropub_post_for_user($user, $micropub_request);
  378. $tweet_id = false;
  379. if($user->twitter_access_token && preg_match('/https?:\/\/(?:www\.)?twitter\.com\/[^\/]+\/status(?:es)?\/(\d+)/', $url, $match)) {
  380. $tweet_id = $match[1];
  381. $twitter = new TwitterOAuth(Config::$twitterClientID, Config::$twitterClientSecret,
  382. $user->twitter_access_token, $user->twitter_token_secret);
  383. $result = $twitter->post('statuses/retweet/'.$tweet_id);
  384. }
  385. return $r;
  386. }
  387. function edit_repost(&$user, $post_url, $repost_of) {
  388. $micropub_request = [
  389. 'action' => 'update',
  390. 'url' => $post_url,
  391. 'replace' => [
  392. 'repost-of' => [$repost_of]
  393. ]
  394. ];
  395. $r = micropub_post_for_user($user, $micropub_request, null, true);
  396. return $r;
  397. }
  398. $app->post('/favorite', function() use($app) {
  399. if($user=require_login($app)) {
  400. $params = $app->request()->params();
  401. $error = false;
  402. if(isset($params['edit']) && $params['edit']) {
  403. $r = edit_favorite($user, $params['edit'], $params['like_of']);
  404. if(isset($r['location']) && $r['location'])
  405. $location = $r['location'];
  406. elseif(in_array($r['code'], [200,201,204]))
  407. $location = $params['edit'];
  408. elseif(in_array($r['code'], [401,403])) {
  409. $location = false;
  410. $error = 'Your Micropub endpoint denied the request. Check that Quill is authorized to update posts.';
  411. } else {
  412. $location = false;
  413. $error = 'Your Micropub endpoint did not return a location header or a recognized response code';
  414. }
  415. } else {
  416. $r = create_favorite($user, $params['like_of']);
  417. $location = $r['location'];
  418. }
  419. $app->response()['Content-type'] = 'application/json';
  420. $app->response()->body(json_encode(array(
  421. 'location' => $location,
  422. 'error' => $r['error'],
  423. 'error_details' => $error,
  424. )));
  425. }
  426. });
  427. $app->post('/repost', function() use($app) {
  428. if($user=require_login($app)) {
  429. $params = $app->request()->params();
  430. $error = false;
  431. if(isset($params['edit']) && $params['edit']) {
  432. $r = edit_repost($user, $params['edit'], $params['repost_of']);
  433. if(isset($r['location']) && $r['location'])
  434. $location = $r['location'];
  435. elseif(in_array($r['code'], [200,201,204]))
  436. $location = $params['edit'];
  437. elseif(in_array($r['code'], [401,403])) {
  438. $location = false;
  439. $error = 'Your Micropub endpoint denied the request. Check that Quill is authorized to update posts.';
  440. } else {
  441. $location = false;
  442. $error = 'Your Micropub endpoint did not return a location header or a recognized response code';
  443. }
  444. } else {
  445. $r = create_repost($user, $params['repost_of']);
  446. $location = $r['location'];
  447. }
  448. $app->response()['Content-type'] = 'application/json';
  449. $app->response()->body(json_encode(array(
  450. 'location' => $location,
  451. 'error' => $r['error'],
  452. 'error_details' => $error,
  453. )));
  454. }
  455. });
  456. $app->get('/reply/preview', function() use($app) {
  457. if($user=require_login($app)) {
  458. $params = $app->request()->params();
  459. if(!isset($params['url']) || !$params['url']) {
  460. return '';
  461. }
  462. $reply_url = trim($params['url']);
  463. if(preg_match('/twtr\.io\/([0-9a-z]+)/i', $reply_url, $match)) {
  464. $twtr = 'https://twitter.com/_/status/' . sxg_to_num($match[1]);
  465. $ch = curl_init($twtr);
  466. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  467. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  468. curl_exec($ch);
  469. $expanded_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
  470. if($expanded_url) $reply_url = $expanded_url;
  471. }
  472. $entry = false;
  473. $xray = [
  474. 'url' => $reply_url
  475. ];
  476. if(preg_match('/twitter\.com\/(?:[^\/]+)\/statuse?s?\/(.+)/', $reply_url, $match)) {
  477. if($user->twitter_access_token) {
  478. $xray['twitter_api_key'] = Config::$twitterClientID;
  479. $xray['twitter_api_secret'] = Config::$twitterClientSecret;
  480. $xray['twitter_access_token'] = $user->twitter_access_token;
  481. $xray['twitter_access_token_secret'] = $user->twitter_token_secret;
  482. }
  483. }
  484. // Pass to X-Ray to see if it can expand the entry
  485. $ch = curl_init('https://xray.p3k.io/parse');
  486. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  487. curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($xray));
  488. $response = curl_exec($ch);
  489. $data = @json_decode($response, true);
  490. if($data && isset($data['data']) && $data['data']['type'] == 'entry') {
  491. $entry = $data['data'];
  492. // Create a nickname based on the author URL
  493. if(array_key_exists('author', $entry)) {
  494. if($entry['author']['url']) {
  495. if(!isset($entry['author']['nickname']) || !$entry['author']['nickname'])
  496. $entry['author']['nickname'] = display_url($entry['author']['url']);
  497. }
  498. }
  499. }
  500. $mentions = [];
  501. if($entry) {
  502. if(array_key_exists('author', $entry)) {
  503. // Find all @-names in the post, as well as the author name
  504. $mentions[] = strtolower($entry['author']['nickname']);
  505. }
  506. if(preg_match_all('/(^|(?<=[\s\/]))@([a-z0-9_]+([a-z0-9_\.]*)?)/i', $entry['content']['text'], $matches)) {
  507. foreach($matches[0] as $nick) {
  508. if(trim($nick,'@') != $user->twitter_username && trim($nick,'@') != display_url($user->url))
  509. $mentions[] = strtolower(trim($nick,'@'));
  510. }
  511. }
  512. $mentions = array_values(array_unique($mentions));
  513. }
  514. $app->response()['Content-type'] = 'application/json';
  515. $app->response()->body(json_encode([
  516. 'canonical_reply_url' => $reply_url,
  517. 'entry' => $entry,
  518. 'mentions' => $mentions
  519. ]));
  520. }
  521. });
  522. $app->get('/edit', function() use($app) {
  523. if($user=require_login($app)) {
  524. $params = $app->request()->params();
  525. if(!isset($params['url']) || !$params['url']) {
  526. $app->response()->body('no URL specified');
  527. }
  528. // Query the micropub endpoint for the source properties
  529. $source = micropub_get($user->micropub_endpoint, [
  530. 'q' => 'source',
  531. 'url' => $params['url']
  532. ], $user->micropub_access_token);
  533. $data = $source['data'];
  534. if(array_key_exists('error', $data)) {
  535. render('edit/error', [
  536. 'title' => 'Error',
  537. 'summary' => 'Your Micropub endpoint returned an error:',
  538. 'error' => $data['error'],
  539. 'error_description' => $data['error_description']
  540. ]);
  541. return;
  542. }
  543. if(!array_key_exists('properties', $data) || !array_key_exists('type', $data)) {
  544. render('edit/error', [
  545. 'title' => 'Error',
  546. 'summary' => '',
  547. 'error' => 'Invalid Response',
  548. 'error_description' => 'Your endpoint did not return "properties" and "type" in the response.'
  549. ]);
  550. return;
  551. }
  552. // Start checking for content types
  553. $type = $data['type'][0];
  554. $error = false;
  555. $url = false;
  556. if($type == 'h-review') {
  557. $url = '/review';
  558. } elseif($type == 'h-event') {
  559. $url = '/event';
  560. } elseif($type != 'h-entry') {
  561. $error = 'This type of post is not supported by any of Quill\'s editing interfaces. Type: '.$type;
  562. } else {
  563. if(array_key_exists('bookmark-of', $data['properties'])) {
  564. $url = '/bookmark';
  565. } elseif(array_key_exists('like-of', $data['properties'])) {
  566. $url = '/favorite';
  567. } elseif(array_key_exists('repost-of', $data['properties'])) {
  568. $url = '/repost';
  569. }
  570. }
  571. if($error) {
  572. render('edit/error', [
  573. 'title' => 'Error',
  574. 'summary' => '',
  575. 'error' => 'There was a problem!',
  576. 'error_description' => $error
  577. ]);
  578. return;
  579. }
  580. // Until all interfaces are complete, show an error here for unsupported ones
  581. if(!in_array($url, ['/favorite','/repost'])) {
  582. render('edit/error', [
  583. 'title' => 'Not Yet Supported',
  584. 'summary' => '',
  585. 'error' => 'Not Yet Supported',
  586. 'error_description' => 'Editing is not yet supported for this type of post.'
  587. ]);
  588. return;
  589. }
  590. $app->redirect($url . '?edit=' . $params['url'], 302);
  591. }
  592. });