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