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