PollPlugin.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. <?php
  2. /**
  3. * StatusNet - the distributed open-source microblogging tool
  4. * Copyright (C) 2011, StatusNet, Inc.
  5. *
  6. * A plugin to enable social-bookmarking functionality
  7. *
  8. * PHP version 5
  9. *
  10. * This program is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU Affero General Public License as published by
  12. * the Free Software Foundation, either version 3 of the License, or
  13. * (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. *
  23. * @category PollPlugin
  24. * @package StatusNet
  25. * @author Brion Vibber <brion@status.net>
  26. * @copyright 2011 StatusNet, Inc.
  27. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
  28. * @link http://status.net/
  29. */
  30. if (!defined('STATUSNET')) {
  31. exit(1);
  32. }
  33. include("../plugins/Poll/classes/Poll.php");
  34. include("../plugins/Poll/classes/Poll_response.php");
  35. include("../plugins/Poll/classes/User_poll_prefs.php");
  36. /**
  37. * Poll plugin main class
  38. *
  39. * @category PollPlugin
  40. * @package StatusNet
  41. * @author Brion Vibber <brionv@status.net>
  42. * @author Evan Prodromou <evan@status.net>
  43. * @copyright 2011 StatusNet, Inc.
  44. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
  45. * @link http://status.net/
  46. */
  47. class PollPlugin extends MicroAppPlugin
  48. {
  49. const PLUGIN_VERSION = '0.1.1';
  50. // @fixme which domain should we use for these namespaces?
  51. const POLL_OBJECT = 'http://activityschema.org/object/poll';
  52. const POLL_RESPONSE_OBJECT = 'http://activityschema.org/object/poll-response';
  53. public $oldSaveNew = true;
  54. /**
  55. * Database schema setup
  56. *
  57. * @return boolean hook value; true means continue processing, false means stop.
  58. * @see ColumnDef
  59. *
  60. * @see Schema
  61. */
  62. public function onCheckSchema()
  63. {
  64. $schema = Schema::get();
  65. $schema->ensureTable('poll', Poll::schemaDef());
  66. $schema->ensureTable('poll_response', Poll_response::schemaDef());
  67. $schema->ensureTable('user_poll_prefs', User_poll_prefs::schemaDef());
  68. return true;
  69. }
  70. /**
  71. * Show the CSS necessary for this plugin
  72. *
  73. * @param Action $action the action being run
  74. *
  75. * @return boolean hook value
  76. */
  77. public function onEndShowStyles($action)
  78. {
  79. $action->cssLink($this->path('css/poll.css'));
  80. return true;
  81. }
  82. /**
  83. * Map URLs to actions
  84. *
  85. * @param URLMapper $m path-to-action mapper
  86. *
  87. * @return boolean hook value; true means continue processing, false means stop.
  88. */
  89. public function onRouterInitialized(URLMapper $m)
  90. {
  91. $m->connect('main/poll/new',
  92. ['action' => 'newpoll']);
  93. $m->connect('main/poll/:id',
  94. ['action' => 'showpoll'],
  95. ['id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}']);
  96. $m->connect('main/poll/response/:id',
  97. ['action' => 'showpollresponse'],
  98. ['id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}']);
  99. $m->connect('main/poll/:id/respond',
  100. ['action' => 'respondpoll'],
  101. ['id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}']);
  102. $m->connect('settings/poll',
  103. ['action' => 'pollsettings']);
  104. return true;
  105. }
  106. /**
  107. * Plugin version data
  108. *
  109. * @param array &$versions array of version data
  110. *
  111. * @return bool true hook value
  112. * @throws Exception
  113. */
  114. public function onPluginVersion(array &$versions): bool
  115. {
  116. $versions[] = array('name' => 'Poll',
  117. 'version' => self::PLUGIN_VERSION,
  118. 'author' => 'Brion Vibber',
  119. 'homepage' => GNUSOCIAL_ENGINE_REPO_URL . 'tree/master/plugins/Poll',
  120. 'rawdescription' =>
  121. // TRANS: Plugin description.
  122. _m('Simple extension for supporting basic polls.'));
  123. return true;
  124. }
  125. public function types()
  126. {
  127. return array(self::POLL_OBJECT, self::POLL_RESPONSE_OBJECT);
  128. }
  129. /**
  130. * When a notice is deleted, delete the related Poll
  131. *
  132. * @param Notice $notice Notice being deleted
  133. *
  134. * @return boolean hook value
  135. */
  136. public function deleteRelated(Notice $notice)
  137. {
  138. $p = Poll::getByNotice($notice);
  139. if (!empty($p)) {
  140. $p->delete();
  141. }
  142. return true;
  143. }
  144. /**
  145. * Save a poll from an activity
  146. *
  147. * @param Activity $activity Activity to save
  148. * @param Profile $profile Profile to use as author
  149. * @param array $options Options to pass to bookmark-saving code
  150. *
  151. * @return Notice resulting notice
  152. * @throws Exception if it failed
  153. */
  154. public function saveNoticeFromActivity(Activity $activity, Profile $profile, array $options = array())
  155. {
  156. // @fixme
  157. common_log(LOG_DEBUG, "XXX activity: " . var_export($activity, true));
  158. common_log(LOG_DEBUG, "XXX profile: " . var_export($profile, true));
  159. common_log(LOG_DEBUG, "XXX options: " . var_export($options, true));
  160. // Ok for now, we can grab stuff from the XML entry directly.
  161. // This won't work when reading from JSON source
  162. if ($activity->entry) {
  163. $pollElements = $activity->entry->getElementsByTagNameNS(self::POLL_OBJECT, 'poll');
  164. $responseElements = $activity->entry->getElementsByTagNameNS(self::POLL_OBJECT, 'response');
  165. if ($pollElements->length) {
  166. $question = '';
  167. $opts = [];
  168. $data = $pollElements->item(0);
  169. foreach ($data->getElementsByTagNameNS(self::POLL_OBJECT, 'question') as $node) {
  170. $question = $node->textContent;
  171. }
  172. foreach ($data->getElementsByTagNameNS(self::POLL_OBJECT, 'option') as $node) {
  173. $opts[] = $node->textContent;
  174. }
  175. try {
  176. $notice = Poll::saveNew($profile, $question, $opts, $options);
  177. common_log(LOG_DEBUG, "Saved Poll from ActivityStream data ok: notice id " . $notice->id);
  178. return $notice;
  179. } catch (Exception $e) {
  180. common_log(LOG_DEBUG, "Poll save from ActivityStream data failed: " . $e->getMessage());
  181. }
  182. } elseif ($responseElements->length) {
  183. $data = $responseElements->item(0);
  184. $pollUri = $data->getAttribute('poll');
  185. $selection = intval($data->getAttribute('selection'));
  186. if (!$pollUri) {
  187. // TRANS: Exception thrown trying to respond to a poll without a poll reference.
  188. throw new Exception(_m('Invalid poll response: No poll reference.'));
  189. }
  190. $poll = Poll::getKV('uri', $pollUri);
  191. if (!$poll) {
  192. // TRANS: Exception thrown trying to respond to a non-existing poll.
  193. throw new Exception(_m('Invalid poll response: Poll is unknown.'));
  194. }
  195. try {
  196. $notice = Poll_response::saveNew($profile, $poll, $selection, $options);
  197. common_log(LOG_DEBUG, "Saved Poll_response ok, notice id: " . $notice->id);
  198. return $notice;
  199. } catch (Exception $e) {
  200. common_log(LOG_DEBUG, "Poll response save fail: " . $e->getMessage());
  201. // TRANS: Exception thrown trying to respond to a non-existing poll.
  202. }
  203. } else {
  204. common_log(LOG_DEBUG, "YYY no poll data");
  205. }
  206. }
  207. // If it didn't return before
  208. throw new ServerException(_m('Failed to save Poll response.'));
  209. }
  210. public function activityObjectFromNotice(Notice $notice)
  211. {
  212. assert($this->isMyNotice($notice));
  213. switch ($notice->object_type) {
  214. case self::POLL_OBJECT:
  215. return $this->activityObjectFromNoticePoll($notice);
  216. case self::POLL_RESPONSE_OBJECT:
  217. return $this->activityObjectFromNoticePollResponse($notice);
  218. default:
  219. // TRANS: Exception thrown when performing an unexpected action on a poll.
  220. // TRANS: %s is the unexpected object type.
  221. throw new Exception(sprintf(_m('Unexpected type for poll plugin: %s.'), $notice->object_type));
  222. }
  223. }
  224. public function activityObjectFromNoticePollResponse(Notice $notice)
  225. {
  226. $object = new ActivityObject();
  227. $object->id = $notice->uri;
  228. $object->type = self::POLL_RESPONSE_OBJECT;
  229. $object->title = $notice->content;
  230. $object->summary = $notice->content;
  231. $object->link = $notice->getUrl();
  232. $response = Poll_response::getByNotice($notice);
  233. if ($response) {
  234. $poll = $response->getPoll();
  235. if ($poll) {
  236. // Stash data to be formatted later by
  237. // $this->activityObjectOutputAtom() or
  238. // $this->activityObjectOutputJson()...
  239. $object->pollSelection = intval($response->selection);
  240. $object->pollUri = $poll->uri;
  241. }
  242. }
  243. return $object;
  244. }
  245. public function activityObjectFromNoticePoll(Notice $notice)
  246. {
  247. $object = new ActivityObject();
  248. $object->id = $notice->uri;
  249. $object->type = self::POLL_OBJECT;
  250. $object->title = $notice->content;
  251. $object->summary = $notice->content;
  252. $object->link = $notice->getUrl();
  253. $poll = Poll::getByNotice($notice);
  254. if ($poll) {
  255. // Stash data to be formatted later by
  256. // $this->activityObjectOutputAtom() or
  257. // $this->activityObjectOutputJson()...
  258. $object->pollQuestion = $poll->question;
  259. $object->pollOptions = $poll->getOptions();
  260. }
  261. return $object;
  262. }
  263. /**
  264. * Called when generating Atom XML ActivityStreams output from an
  265. * ActivityObject belonging to this plugin. Gives the plugin
  266. * a chance to add custom output.
  267. *
  268. * Note that you can only add output of additional XML elements,
  269. * not change existing stuff here.
  270. *
  271. * If output is already handled by the base Activity classes,
  272. * you can leave this base implementation as a no-op.
  273. *
  274. * @param ActivityObject $obj
  275. * @param XMLOutputter $out to add elements at end of object
  276. */
  277. public function activityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
  278. {
  279. if (isset($obj->pollQuestion)) {
  280. /**
  281. * <poll:poll xmlns:poll="http://apinamespace.org/activitystreams/object/poll">
  282. * <poll:question>Who wants a poll question?</poll:question>
  283. * <poll:option>Option one</poll:option>
  284. * <poll:option>Option two</poll:option>
  285. * <poll:option>Option three</poll:option>
  286. * </poll:poll>
  287. */
  288. $data = array('xmlns:poll' => self::POLL_OBJECT);
  289. $out->elementStart('poll:poll', $data);
  290. $out->element('poll:question', array(), $obj->pollQuestion);
  291. foreach ($obj->pollOptions as $opt) {
  292. $out->element('poll:option', array(), $opt);
  293. }
  294. $out->elementEnd('poll:poll');
  295. }
  296. if (isset($obj->pollSelection)) {
  297. /**
  298. * <poll:response xmlns:poll="http://apinamespace.org/activitystreams/object/poll">
  299. * poll="http://..../poll/...."
  300. * selection="3" />
  301. */
  302. $data = array('xmlns:poll' => self::POLL_OBJECT,
  303. 'poll' => $obj->pollUri,
  304. 'selection' => $obj->pollSelection);
  305. $out->element('poll:response', $data, '');
  306. }
  307. }
  308. /**
  309. * Called when generating JSON ActivityStreams output from an
  310. * ActivityObject belonging to this plugin. Gives the plugin
  311. * a chance to add custom output.
  312. *
  313. * Modify the array contents to your heart's content, and it'll
  314. * all get serialized out as JSON.
  315. *
  316. * If output is already handled by the base Activity classes,
  317. * you can leave this base implementation as a no-op.
  318. *
  319. * @param ActivityObject $obj
  320. * @param array &$out JSON-targeted array which can be modified
  321. */
  322. public function activityObjectOutputJson(ActivityObject $obj, array &$out)
  323. {
  324. common_log(LOG_DEBUG, 'QQQ: ' . var_export($obj, true));
  325. if (isset($obj->pollQuestion)) {
  326. /**
  327. * "poll": {
  328. * "question": "Who wants a poll question?",
  329. * "options": [
  330. * "Option 1",
  331. * "Option 2",
  332. * "Option 3"
  333. * ]
  334. * }
  335. */
  336. $data = array('question' => $obj->pollQuestion,
  337. 'options' => array());
  338. foreach ($obj->pollOptions as $opt) {
  339. $data['options'][] = $opt;
  340. }
  341. $out['poll'] = $data;
  342. }
  343. if (isset($obj->pollSelection)) {
  344. /**
  345. * "pollResponse": {
  346. * "poll": "http://..../poll/....",
  347. * "selection": 3
  348. * }
  349. */
  350. $data = array('poll' => $obj->pollUri,
  351. 'selection' => $obj->pollSelection);
  352. $out['pollResponse'] = $data;
  353. }
  354. }
  355. public function entryForm($out)
  356. {
  357. return new NewPollForm($out);
  358. }
  359. // @fixme is this from parent?
  360. public function tag()
  361. {
  362. return 'poll';
  363. }
  364. public function appTitle()
  365. {
  366. // TRANS: Application title.
  367. return _m('APPTITLE', 'Poll');
  368. }
  369. public function onStartAddNoticeReply($nli, $parent, $child)
  370. {
  371. // Filter out any poll responses
  372. if ($parent->object_type == self::POLL_OBJECT &&
  373. $child->object_type == self::POLL_RESPONSE_OBJECT) {
  374. return false;
  375. }
  376. return true;
  377. }
  378. // Hide poll responses for @chuck
  379. public function onEndNoticeWhoGets($notice, &$ni)
  380. {
  381. if ($notice->object_type == self::POLL_RESPONSE_OBJECT) {
  382. foreach ($ni as $id => $source) {
  383. $user = User::getKV('id', $id);
  384. if (!empty($user)) {
  385. $pollPrefs = User_poll_prefs::getKV('user_id', $user->id);
  386. if (!empty($pollPrefs) && ($pollPrefs->hide_responses)) {
  387. unset($ni[$id]);
  388. }
  389. }
  390. }
  391. }
  392. return true;
  393. }
  394. /**
  395. * Menu item for personal subscriptions/groups area
  396. *
  397. * @param Action $action action being executed
  398. *
  399. * @return boolean hook return
  400. */
  401. public function onEndAccountSettingsNav($action)
  402. {
  403. $action_name = $action->trimmed('action');
  404. $action->menuItem(
  405. common_local_url('pollsettings'),
  406. // TRANS: Poll plugin menu item on user settings page.
  407. _m('MENU', 'Polls'),
  408. // TRANS: Poll plugin tooltip for user settings menu item.
  409. _m('Configure poll behavior'),
  410. $action_name === 'pollsettings'
  411. );
  412. return true;
  413. }
  414. protected function showNoticeContent(Notice $stored, HTMLOutputter $out, Profile $scoped = null)
  415. {
  416. if ($stored->object_type == self::POLL_RESPONSE_OBJECT) {
  417. parent::showNoticeContent($stored, $out, $scoped);
  418. return;
  419. }
  420. // If the stored notice is a POLL_OBJECT
  421. $poll = Poll::getByNotice($stored);
  422. if ($poll instanceof Poll) {
  423. if (!$scoped instanceof Profile || $poll->getResponse($scoped) instanceof Poll_response) {
  424. // Either the user is not logged in or it has already responded; show the results.
  425. $form = new PollResultForm($poll, $out);
  426. } else {
  427. $form = new PollResponseForm($poll, $out);
  428. }
  429. $form->show();
  430. } else {
  431. // TRANS: Error text displayed if no poll data could be found.
  432. $out->text(_m('Poll data is missing'));
  433. }
  434. }
  435. }