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