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