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.

703 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 = $edit_data['repost-of'][0];
  196. print_r($edit_data);
  197. if(is_string($edit_data['repost-of'][0])) {
  198. $repost_of = $repost;
  199. } elseif(is_array($repost)) {
  200. if(array_key_exists('type', $repost) && in_array('h-cite', $repost)) {
  201. if(array_key_exists('url', $repost['properties'])) {
  202. $repost_of = $repost['properties']['url'][0];
  203. }
  204. } else {
  205. // Error
  206. }
  207. } else {
  208. // Error: don't know what type of post this is
  209. }
  210. }
  211. } else {
  212. $edit_data = false;
  213. $url = false;
  214. }
  215. render('new-repost', array(
  216. 'title' => 'New Repost',
  217. 'repost_of' => $repost_of,
  218. 'token' => generate_login_token(),
  219. 'authorizing' => false,
  220. 'url' => $url,
  221. ));
  222. }
  223. });
  224. $app->post('/prefs', function() use($app) {
  225. if($user=require_login($app)) {
  226. $params = $app->request()->params();
  227. $user->location_enabled = $params['enabled'];
  228. $user->save();
  229. }
  230. $app->response()['Content-type'] = 'application/json';
  231. $app->response()->body(json_encode(array(
  232. 'result' => 'ok'
  233. )));
  234. });
  235. $app->post('/prefs/timezone', function() use($app) {
  236. // Called when the interface finds the user's location.
  237. // Look up the timezone for this location and store it as their default.
  238. $timezone = false;
  239. if($user=require_login($app)) {
  240. $params = $app->request()->params();
  241. $timezone = p3k\Timezone::timezone_for_location($params['latitude'], $params['longitude']);
  242. if($timezone) {
  243. $user->default_timezone = $timezone;
  244. $user->save();
  245. }
  246. }
  247. $app->response()['Content-type'] = 'application/json';
  248. $app->response()->body(json_encode(array(
  249. 'result' => 'ok',
  250. 'timezone' => $timezone,
  251. )));
  252. });
  253. $app->get('/add-to-home', function() use($app) {
  254. $params = $app->request()->params();
  255. header("Cache-Control: no-cache, must-revalidate");
  256. if(array_key_exists('token', $params) && !session('add-to-home-started')) {
  257. unset($_SESSION['add-to-home-started']);
  258. // Verify the token and sign the user in
  259. try {
  260. $data = JWT::decode($params['token'], Config::$jwtSecret, array('HS256'));
  261. $_SESSION['user_id'] = $data->user_id;
  262. $_SESSION['me'] = $data->me;
  263. $app->redirect('/new', 302);
  264. } catch(DomainException $e) {
  265. header('X-Error: DomainException');
  266. $app->redirect('/', 302);
  267. } catch(UnexpectedValueException $e) {
  268. header('X-Error: UnexpectedValueException');
  269. $app->redirect('/', 302);
  270. }
  271. } else {
  272. if($user=require_login($app)) {
  273. if(array_key_exists('start', $params)) {
  274. $_SESSION['add-to-home-started'] = true;
  275. $token = JWT::encode(array(
  276. 'user_id' => $_SESSION['user_id'],
  277. 'me' => $_SESSION['me'],
  278. 'created_at' => time()
  279. ), Config::$jwtSecret);
  280. $app->redirect('/add-to-home?token='.$token, 302);
  281. } else {
  282. unset($_SESSION['add-to-home-started']);
  283. render('add-to-home', array('title' => 'Quill'));
  284. }
  285. }
  286. }
  287. });
  288. $app->get('/email', function() use($app) {
  289. if($user=require_login($app)) {
  290. $test_response = '';
  291. if($user->last_micropub_response) {
  292. try {
  293. if(@json_decode($user->last_micropub_response)) {
  294. $d = json_decode($user->last_micropub_response);
  295. $test_response = $d->response;
  296. }
  297. } catch(Exception $e) {
  298. }
  299. }
  300. if(!$user->email_username) {
  301. $host = parse_url($user->url, PHP_URL_HOST);
  302. $user->email_username = $host . '.' . rand(100000,999999);
  303. $user->save();
  304. }
  305. render('email', array(
  306. 'title' => 'Post-by-Email',
  307. 'micropub_endpoint' => $user->micropub_endpoint,
  308. 'test_response' => $test_response,
  309. 'user' => $user
  310. ));
  311. }
  312. });
  313. $app->get('/settings', function() use($app) {
  314. if($user=require_login($app)) {
  315. render('settings', [
  316. 'title' => 'Settings',
  317. 'user' => $user,
  318. 'authorizing' => false
  319. ]);
  320. }
  321. });
  322. $app->post('/settings/save', function() use($app) {
  323. if($user=require_login($app)) {
  324. $params = $app->request()->params();
  325. if(array_key_exists('html_content', $params))
  326. $user->micropub_optin_html_content = $params['html_content'] ? 1 : 0;
  327. if(array_key_exists('slug_field', $params) && $params['slug_field'])
  328. $user->micropub_slug_field = $params['slug_field'];
  329. if(array_key_exists('syndicate_field', $params) && $params['syndicate_field']) {
  330. if(in_array($params['syndicate_field'], ['syndicate-to','mp-syndicate-to']))
  331. $user->micropub_syndicate_field = $params['syndicate_field'];
  332. }
  333. $user->save();
  334. $app->response()['Content-type'] = 'application/json';
  335. $app->response()->body(json_encode(array(
  336. 'result' => 'ok'
  337. )));
  338. }
  339. });
  340. $app->post('/settings/html-content', function() use($app) {
  341. if($user=require_login($app)) {
  342. $params = $app->request()->params();
  343. $user->micropub_optin_html_content = $params['html'] ? 1 : 0;
  344. $user->save();
  345. $app->response()['Content-type'] = 'application/json';
  346. $app->response()->body(json_encode(array(
  347. 'html' => $user->micropub_optin_html_content
  348. )));
  349. }
  350. });
  351. $app->get('/settings/html-content', function() use($app) {
  352. if($user=require_login($app)) {
  353. $app->response()['Content-type'] = 'application/json';
  354. $app->response()->body(json_encode(array(
  355. 'html' => $user->micropub_optin_html_content
  356. )));
  357. }
  358. });
  359. function create_favorite(&$user, $url) {
  360. $micropub_request = array(
  361. 'like-of' => $url
  362. );
  363. $r = micropub_post_for_user($user, $micropub_request);
  364. $tweet_id = false;
  365. // POSSE favorites to Twitter
  366. if($user->twitter_access_token && preg_match('/https?:\/\/(?:www\.)?twitter\.com\/[^\/]+\/status(?:es)?\/(\d+)/', $url, $match)) {
  367. $tweet_id = $match[1];
  368. $twitter = new TwitterOAuth(Config::$twitterClientID, Config::$twitterClientSecret,
  369. $user->twitter_access_token, $user->twitter_token_secret);
  370. $result = $twitter->post('favorites/create', array(
  371. 'id' => $tweet_id
  372. ));
  373. }
  374. return $r;
  375. }
  376. function edit_favorite(&$user, $post_url, $like_of) {
  377. $micropub_request = [
  378. 'action' => 'update',
  379. 'url' => $post_url,
  380. 'replace' => [
  381. 'like-of' => [$like_of]
  382. ]
  383. ];
  384. $r = micropub_post_for_user($user, $micropub_request, null, true);
  385. return $r;
  386. }
  387. function create_repost(&$user, $url) {
  388. $micropub_request = array(
  389. 'repost-of' => $url
  390. );
  391. $r = micropub_post_for_user($user, $micropub_request);
  392. $tweet_id = false;
  393. if($user->twitter_access_token && preg_match('/https?:\/\/(?:www\.)?twitter\.com\/[^\/]+\/status(?:es)?\/(\d+)/', $url, $match)) {
  394. $tweet_id = $match[1];
  395. $twitter = new TwitterOAuth(Config::$twitterClientID, Config::$twitterClientSecret,
  396. $user->twitter_access_token, $user->twitter_token_secret);
  397. $result = $twitter->post('statuses/retweet/'.$tweet_id);
  398. }
  399. return $r;
  400. }
  401. function edit_repost(&$user, $post_url, $repost_of) {
  402. $micropub_request = [
  403. 'action' => 'update',
  404. 'url' => $post_url,
  405. 'replace' => [
  406. 'repost-of' => [$repost_of]
  407. ]
  408. ];
  409. $r = micropub_post_for_user($user, $micropub_request, null, true);
  410. return $r;
  411. }
  412. $app->post('/favorite', function() use($app) {
  413. if($user=require_login($app)) {
  414. $params = $app->request()->params();
  415. $error = false;
  416. if(isset($params['edit']) && $params['edit']) {
  417. $r = edit_favorite($user, $params['edit'], $params['like_of']);
  418. if(isset($r['location']) && $r['location'])
  419. $location = $r['location'];
  420. elseif(in_array($r['code'], [200,201,204]))
  421. $location = $params['edit'];
  422. elseif(in_array($r['code'], [401,403])) {
  423. $location = false;
  424. $error = 'Your Micropub endpoint denied the request. Check that Quill is authorized to update posts.';
  425. } else {
  426. $location = false;
  427. $error = 'Your Micropub endpoint did not return a location header or a recognized response code';
  428. }
  429. } else {
  430. $r = create_favorite($user, $params['like_of']);
  431. $location = $r['location'];
  432. }
  433. $app->response()['Content-type'] = 'application/json';
  434. $app->response()->body(json_encode(array(
  435. 'location' => $location,
  436. 'error' => $r['error'],
  437. 'error_details' => $error,
  438. )));
  439. }
  440. });
  441. $app->post('/repost', function() use($app) {
  442. if($user=require_login($app)) {
  443. $params = $app->request()->params();
  444. $error = false;
  445. if(isset($params['edit']) && $params['edit']) {
  446. $r = edit_repost($user, $params['edit'], $params['repost_of']);
  447. if(isset($r['location']) && $r['location'])
  448. $location = $r['location'];
  449. elseif(in_array($r['code'], [200,201,204]))
  450. $location = $params['edit'];
  451. elseif(in_array($r['code'], [401,403])) {
  452. $location = false;
  453. $error = 'Your Micropub endpoint denied the request. Check that Quill is authorized to update posts.';
  454. } else {
  455. $location = false;
  456. $error = 'Your Micropub endpoint did not return a location header or a recognized response code';
  457. }
  458. } else {
  459. $r = create_repost($user, $params['repost_of']);
  460. $location = $r['location'];
  461. }
  462. $app->response()['Content-type'] = 'application/json';
  463. $app->response()->body(json_encode(array(
  464. 'location' => $location,
  465. 'error' => $r['error'],
  466. 'error_details' => $error,
  467. )));
  468. }
  469. });
  470. $app->get('/reply/preview', function() use($app) {
  471. if($user=require_login($app)) {
  472. $params = $app->request()->params();
  473. if(!isset($params['url']) || !$params['url']) {
  474. return '';
  475. }
  476. $reply_url = trim($params['url']);
  477. if(preg_match('/twtr\.io\/([0-9a-z]+)/i', $reply_url, $match)) {
  478. $twtr = 'https://twitter.com/_/status/' . sxg_to_num($match[1]);
  479. $ch = curl_init($twtr);
  480. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  481. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  482. curl_exec($ch);
  483. $expanded_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
  484. if($expanded_url) $reply_url = $expanded_url;
  485. }
  486. $entry = false;
  487. $xray = [
  488. 'url' => $reply_url
  489. ];
  490. if(preg_match('/twitter\.com\/(?:[^\/]+)\/statuse?s?\/(.+)/', $reply_url, $match)) {
  491. if($user->twitter_access_token) {
  492. $xray['twitter_api_key'] = Config::$twitterClientID;
  493. $xray['twitter_api_secret'] = Config::$twitterClientSecret;
  494. $xray['twitter_access_token'] = $user->twitter_access_token;
  495. $xray['twitter_access_token_secret'] = $user->twitter_token_secret;
  496. }
  497. }
  498. // Pass to X-Ray to see if it can expand the entry
  499. $ch = curl_init('https://xray.p3k.io/parse');
  500. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  501. curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($xray));
  502. $response = curl_exec($ch);
  503. $data = @json_decode($response, true);
  504. if($data && isset($data['data']) && $data['data']['type'] == 'entry') {
  505. $entry = $data['data'];
  506. // Create a nickname based on the author URL
  507. if(array_key_exists('author', $entry)) {
  508. if($entry['author']['url']) {
  509. if(!isset($entry['author']['nickname']) || !$entry['author']['nickname'])
  510. $entry['author']['nickname'] = display_url($entry['author']['url']);
  511. }
  512. }
  513. }
  514. $mentions = [];
  515. if($entry) {
  516. if(array_key_exists('author', $entry)) {
  517. // Find all @-names in the post, as well as the author name
  518. $mentions[] = strtolower($entry['author']['nickname']);
  519. }
  520. if(preg_match_all('/(^|(?<=[\s\/]))@([a-z0-9_]+([a-z0-9_\.]*)?)/i', $entry['content']['text'], $matches)) {
  521. foreach($matches[0] as $nick) {
  522. if(trim($nick,'@') != $user->twitter_username && trim($nick,'@') != display_url($user->url))
  523. $mentions[] = strtolower(trim($nick,'@'));
  524. }
  525. }
  526. $mentions = array_values(array_unique($mentions));
  527. }
  528. $app->response()['Content-type'] = 'application/json';
  529. $app->response()->body(json_encode([
  530. 'canonical_reply_url' => $reply_url,
  531. 'entry' => $entry,
  532. 'mentions' => $mentions
  533. ]));
  534. }
  535. });
  536. $app->get('/edit', function() use($app) {
  537. if($user=require_login($app)) {
  538. $params = $app->request()->params();
  539. if(!isset($params['url']) || !$params['url']) {
  540. $app->response()->body('no URL specified');
  541. }
  542. // Query the micropub endpoint for the source properties
  543. $source = micropub_get($user->micropub_endpoint, [
  544. 'q' => 'source',
  545. 'url' => $params['url']
  546. ], $user->micropub_access_token);
  547. $data = $source['data'];
  548. if(array_key_exists('error', $data)) {
  549. render('edit/error', [
  550. 'title' => 'Error',
  551. 'summary' => 'Your Micropub endpoint returned an error:',
  552. 'error' => $data['error'],
  553. 'error_description' => $data['error_description']
  554. ]);
  555. return;
  556. }
  557. if(!array_key_exists('properties', $data) || !array_key_exists('type', $data)) {
  558. render('edit/error', [
  559. 'title' => 'Error',
  560. 'summary' => '',
  561. 'error' => 'Invalid Response',
  562. 'error_description' => 'Your endpoint did not return "properties" and "type" in the response.'
  563. ]);
  564. return;
  565. }
  566. // Start checking for content types
  567. $type = $data['type'][0];
  568. $error = false;
  569. $url = false;
  570. if($type == 'h-review') {
  571. $url = '/review';
  572. } elseif($type == 'h-event') {
  573. $url = '/event';
  574. } elseif($type != 'h-entry') {
  575. $error = 'This type of post is not supported by any of Quill\'s editing interfaces. Type: '.$type;
  576. } else {
  577. if(array_key_exists('bookmark-of', $data['properties'])) {
  578. $url = '/bookmark';
  579. } elseif(array_key_exists('like-of', $data['properties'])) {
  580. $url = '/favorite';
  581. } elseif(array_key_exists('repost-of', $data['properties'])) {
  582. $url = '/repost';
  583. }
  584. }
  585. if($error) {
  586. render('edit/error', [
  587. 'title' => 'Error',
  588. 'summary' => '',
  589. 'error' => 'There was a problem!',
  590. 'error_description' => $error
  591. ]);
  592. return;
  593. }
  594. // Until all interfaces are complete, show an error here for unsupported ones
  595. if(!in_array($url, ['/favorite','/repost'])) {
  596. render('edit/error', [
  597. 'title' => 'Not Yet Supported',
  598. 'summary' => '',
  599. 'error' => 'Not Yet Supported',
  600. 'error_description' => 'Editing is not yet supported for this type of post.'
  601. ]);
  602. return;
  603. }
  604. $app->redirect($url . '?edit=' . $params['url'], 302);
  605. }
  606. });