Mirror of Quill
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.

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