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