PollPlugin.php 16 KB

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