]> git.mxchange.org Git - friendica-addons.git/blob - windowsphonepush/windowsphonepush.php
Merge branch '3.6-rc'
[friendica-addons.git] / windowsphonepush / windowsphonepush.php
1 <?php
2 /**
3  * Name: WindowsPhonePush
4  * Description: Enable push notification to send information to Friendica Mobile app on Windows phone (count of unread timeline entries, text of last posting - if wished by user)
5  * Version: 2.0
6  * Author: Gerhard Seeber <http://friendica.seeber.at/profile/admin>
7  * 
8  * 
9  * Pre-requisite: Windows Phone mobile device (at least WP 7.0)
10  *                Friendica mobile app on Windows Phone
11  *
12  * When addon is installed, the system calls the addon
13  * name_install() function, located in 'addon/name/name.php',
14  * where 'name' is the name of the addon.
15  * If the addon is removed from the configuration list, the 
16  * system will call the name_uninstall() function.
17  *
18  * Version history:
19  * 1.1  : addon crashed on php versions >= 5.4 as of removed deprecated call-time 
20  *        pass-by-reference used in function calls within function windowsphonepush_content
21  * 2.0  : adaption for supporting emphasizing new entries in app (count on tile cannot be read out,
22  *        so we need to retrieve counter through show_settings secondly). Provide new function for 
23  *        calling from app to set the counter back after start (if user starts again before cronjob
24  *        sets the counter back
25  *        count only unseen elements which are not type=activity (likes and dislikes not seen as new elements)
26  */
27
28 use Friendica\App;
29 use Friendica\Content\Text\BBCode;
30 use Friendica\Core\Addon;
31 use Friendica\Core\L10n;
32 use Friendica\Core\PConfig;
33 use Friendica\Model\User;
34
35 function windowsphonepush_install()
36 {
37         /* Our addon will attach in three places.
38          * The first is within cron - so the push notifications will be
39          * sent every 10 minutes (or whatever is set in crontab).
40          *
41          */
42         Addon::registerHook('cron', 'addon/windowsphonepush/windowsphonepush.php', 'windowsphonepush_cron');
43
44         /* Then we'll attach into the addon settings page, and also the
45          * settings post hook so that we can create and update
46          * user preferences. User shall be able to activate the addon and
47          * define whether he allows pushing first characters of item text
48          *
49          */
50         Addon::registerHook('addon_settings', 'addon/windowsphonepush/windowsphonepush.php', 'windowsphonepush_settings');
51         Addon::registerHook('addon_settings_post', 'addon/windowsphonepush/windowsphonepush.php', 'windowsphonepush_settings_post');
52
53         logger("installed windowsphonepush");
54 }
55
56
57 function windowsphonepush_uninstall() {
58
59         /**
60          *
61          * uninstall unregisters any hooks created with register_hook
62          * during install. Don't delete data in table `pconfig`.
63          *
64          */
65         Addon::unregisterHook('cron', 'addon/windowsphonepush/windowsphonepush.php', 'windowsphonepush_cron');
66         Addon::unregisterHook('addon_settings', 'addon/windowsphonepush/windowsphonepush.php', 'windowsphonepush_settings');
67         Addon::unregisterHook('addon_settings_post', 'addon/windowsphonepush/windowsphonepush.php', 'windowsphonepush_settings_post');
68
69         logger("removed windowsphonepush");
70 }
71
72
73 /* declare the windowsphonepush function so that /windowsphonepush url requests will land here */
74 function windowsphonepush_module() {}
75
76
77 /**
78  *
79  * Callback from the settings post function.
80  * $post contains the $_POST array.
81  * We will make sure we've got a valid user account
82  * and if so set our configuration setting for this person.
83  *
84  */
85 function windowsphonepush_settings_post($a,$post) {
86         if(! local_user() || (! x($_POST,'windowsphonepush-submit')))
87                 return;
88         $enable = intval($_POST['windowsphonepush']);
89         set_pconfig(local_user(),'windowsphonepush','enable',$enable);
90
91         if($enable) {
92                 set_pconfig(local_user(),'windowsphonepush','counterunseen', 0);
93         }
94
95         set_pconfig(local_user(),'windowsphonepush','senditemtext',intval($_POST['windowsphonepush-senditemtext']));
96
97         info(L10n::t('WindowsPhonePush settings updated.') . EOL);
98 }
99
100 /* Called from the Addon Setting form.
101  * Add our own settings info to the page.
102  *
103  */
104 function windowsphonepush_settings(&$a,&$s) {
105
106         if(! local_user())
107                 return;
108
109         /* Add our stylesheet to the page so we can make our settings look nice */
110         $a->page['htmlhead'] .= '<link rel="stylesheet"  type="text/css" href="' . $a->get_baseurl() . '/addon/windowsphonepush/windowsphonepush.css' . '" media="all" />' . "\r\n";
111
112         /* Get the current state of our config variables */
113         $enabled = get_pconfig(local_user(),'windowsphonepush','enable');
114         $checked_enabled = (($enabled) ? ' checked="checked" ' : '');
115
116         $senditemtext = get_pconfig(local_user(), 'windowsphonepush', 'senditemtext');
117         $checked_senditemtext = (($senditemtext) ? ' checked="checked" ' : '');
118
119         $device_url = get_pconfig(local_user(), 'windowsphonepush', 'device_url');
120
121         /* Add some HTML to the existing form */
122         $s .= '<div class="settings-block">';
123         $s .= '<h3>' . L10n::t('WindowsPhonePush Settings') . '</h3>';
124
125         $s .= '<div id="windowsphonepush-enable-wrapper">';
126         $s .= '<label id="windowsphonepush-enable-label" for="windowsphonepush-enable-chk">' . L10n::t('Enable WindowsPhonePush Addon') . '</label>';
127         $s .= '<input id="windowsphonepush-enable-chk" type="checkbox" name="windowsphonepush" value="1" ' . $checked_enabled . '/>';
128         $s .= '</div><div class="clear"></div>';
129
130         $s .= '<div id="windowsphonepush-senditemtext-wrapper">';
131         $s .= '<label id="windowsphonepush-senditemtext-label" for="windowsphonepush-senditemtext-chk">' . L10n::t('Push text of new item') . '</label>';
132         $s .= '<input id="windowsphonepush-senditemtext-chk" type="checkbox" name="windowsphonepush-senditemtext" value="1" ' . $checked_senditemtext . '/>';
133         $s .= '</div><div class="clear"></div>';
134
135         /* provide a submit button - enable und senditemtext can be changed by the user */
136         $s .= '<div class="settings-submit-wrapper" ><input type="submit" id="windowsphonepush-submit" name="windowsphonepush-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div><div class="clear"></div>';
137
138         /* provide further read-only information concerning the addon (useful for */
139         $s .= '<div id="windowsphonepush-device_url-wrapper">';
140         $s .= '<label id="windowsphonepush-device_url-label" for="windowsphonepush-device_url-text">Device-URL</label>';
141         $s .= '<input id="windowsphonepush-device_url-text" type="text" readonly value=' . $device_url . '/>';
142         $s .= '</div><div class="clear"></div></div>';
143         
144         return;
145
146 }
147
148 /* Cron function used to regularly check all users on the server with active windowsphonepushaddon and send
149  * notifications to the Microsoft servers and consequently to the Windows Phone device
150  *
151  */
152 function windowsphonepush_cron()
153 {
154         // retrieve all UID's for which the addon windowsphonepush is enabled and loop through every user
155         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'windowsphonepush' AND `k` = 'enable' AND `v` = 1");
156         if(count($r)) {
157                 foreach($r as $rr) {
158                         // load stored information for the user-id of the current loop
159                         $device_url = get_pconfig($rr['uid'], 'windowsphonepush', 'device_url');
160                         $lastpushid = get_pconfig($rr['uid'], 'windowsphonepush', 'lastpushid');
161
162                         // pushing only possible if device_url (the URI on Microsoft server) is available or not "NA" (which will be sent 
163                         // by app if user has switched the server setting in app - sending blank not possible as this would return an update error)
164                         if (( $device_url == "" ) || ( $device_url == "NA" )) {
165                                 // no Device-URL for the user availabe, but addon is enabled --> write info to Logger
166                                 logger("WARN: windowsphonepush is enable for user " . $rr['uid'] . ", but no Device-URL is specified for the user.");
167                         } else {
168                                 // retrieve the number of unseen items and the id of the latest one (if there are more than 
169                                 // one new entries since last poller run, only the latest one will be pushed)
170                                 $count = q("SELECT count(`id`) as count, max(`id`) as max FROM `item` WHERE `unseen` = 1 AND `type` <> 'activity' AND `uid` = %d",
171                                         intval($rr['uid'])
172                                 );
173
174                                 // send number of unseen items to the device (the number will be displayed on Start screen until 
175                                 // App will be started by user) - this update will be sent every 10 minutes to update the number to 0 if 
176                                 // user has loaded the timeline through app or website
177                                 $res_tile = send_tile_update($device_url, "", $count[0]['count'], "");
178                                 switch (trim($res_tile)) {
179                                         case "Received":
180                                                 // ok, count has been pushed, let's save it in personal settings 
181                                                 set_pconfig($rr['uid'], 'windowsphonepush', 'counterunseen', $count[0]['count']);
182                                                 break;
183                                         case "QueueFull":
184                                                 // maximum of 30 messages reached, server rejects any further push notification until device reconnects
185                                                 logger("INFO: Device-URL '" . $device_url . "' returns a QueueFull.");
186                                                 break;
187                                         case "Suppressed":
188                                                 // notification received and dropped as something in app was not enabled
189                                                 logger("WARN. Device-URL '" . $device_url . "' returns a Suppressed. Unexpected error in Mobile App?");
190                                                 break;
191                                         case "Dropped":
192                                                 // mostly combines with Expired, in that case Device-URL will be deleted from pconfig (function send_push)
193                                                 break;
194                                         default:
195                                                 // error, mostly called by "" which means that the url (not "" which has been checked)
196                                                 // didn't not received Microsoft Notification Server -> wrong url
197                                                 logger("ERROR: specified Device-URL '" . $device_url . "' didn't produced any response.");
198                                 }
199
200                                 // additionally user receives the text of the newest item (function checks against last successfully pushed item)
201                                 if (intval($count[0]['max']) > intval($lastpushid)) {
202                                         // user can define if he wants to see the text of the item in the push notification
203                                         // this has been implemented as the device_url is not a https uri (not so secure)
204                                         $senditemtext = get_pconfig($rr['uid'], 'windowsphonepush', 'senditemtext');
205                                         if ($senditemtext == 1) {
206                                                 // load item with the max id
207                                                 $item = q("SELECT `author-name` as author, `body` as body FROM `item` where `id` = %d",
208                                                         intval($count[0]['max'])
209                                                 );
210
211                                                 // as user allows to send the item, we want to show the sender of the item in the toast
212                                                 // toasts are limited to one line, therefore place is limited - author shall be in 
213                                                 // max. 15 chars (incl. dots); author is displayed in bold font
214                                                 $author = $item[0]['author'];
215                                                 $author = ((strlen($author) > 12) ? substr($author, 0, 12) . "..." : $author);
216
217                                                 // normally we show the body of the item, however if it is an url or an image we cannot
218                                                 // show this in the toast (only test), therefore changing to an alternate text 
219                                                 // Otherwise BBcode-Tags will be eliminated and plain text cutted to 140 chars (incl. dots)
220                                                 // BTW: information only possible in English
221                                                 $body = $item[0]['body'];
222                                                 if (substr($body, 0, 4) == "[url") 
223                                                         $body = "URL/Image ...";
224                                                 } else {
225                                                         require_once("include/html2plain.php");
226
227                                                         $body = BBCode::convert($body, false, 2, true);
228                                                         $body = html2plain($body, 0);
229                                                         $body = ((strlen($body) > 137) ? substr($body, 0, 137) . "..." : $body);
230                                                 }
231                                         } else {
232                                         // if user wishes higher privacy, we only display "Friendica - New timeline entry arrived"
233                                                 $author = "Friendica";
234                                                 $body = "New timeline entry arrived ...";
235                                         }
236                                         // only if toast push notification returns the Notification status "Received" we will update th settings with the 
237                                         // new indicator max-id is checked against (QueueFull, Suppressed, N/A, Dropped shall qualify to resend
238                                         // the push notification some minutes later (BTW: if resulting in Expired for subscription status the 
239                                         // device_url will be deleted (no further try on this url, see send_push)
240                                         // further log information done on count pushing with send_tile (see above)
241                                         $res_toast = send_toast($device_url, $author, $body);
242                                         if (trim($res_toast) === 'Received') {
243                                                 set_pconfig($rr['uid'], 'windowsphonepush', 'lastpushid', $count[0]['max']);
244                                         }                               
245                                 }
246                         }
247                 }
248         }
249 }
250
251
252 /* 
253  *
254  * Tile push notification change the number in the icon of the App in Start Screen of
255  * a Windows Phone Device, Image could be changed, not used for App "Friendica Mobile"
256  * 
257  */
258 function send_tile_update($device_url, $image_url, $count, $title, $priority = 1) {
259         $msg = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" .
260                 "<wp:Notification xmlns:wp=\"WPNotification\">" .
261                         "<wp:Tile>".
262                                 "<wp:BackgroundImage>" . $image_url . "</wp:BackgroundImage>" .
263                                 "<wp:Count>" . $count . "</wp:Count>" .
264                                 "<wp:Title>" . $title . "</wp:Title>" .
265                         "</wp:Tile> " .
266                 "</wp:Notification>";
267
268         $result = send_push($device_url, array(
269                 'X-WindowsPhone-Target: token',
270                 'X-NotificationClass: ' . $priority,
271                 ), $msg);
272         return $result;
273 }
274
275 /*
276  * 
277  * Toast push notification send information to the top of the display
278  * if the user is not currently using the Friendica Mobile App, however
279  * there is only one line for displaying the information
280  *
281  */
282 function send_toast($device_url, $title, $message, $priority = 2) {
283         $msg = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" . 
284                 "<wp:Notification xmlns:wp=\"WPNotification\">" .
285                         "<wp:Toast>" .
286                                 "<wp:Text1>" . $title . "</wp:Text1>" .
287                                 "<wp:Text2>" . $message . "</wp:Text2>" .
288                                 "<wp:Param></wp:Param>" . 
289                         "</wp:Toast>" .
290                 "</wp:Notification>";
291
292         $result = send_push($device_url, array(
293                 'X-WindowsPhone-Target: toast',
294                 'X-NotificationClass: ' . $priority, 
295                 ), $msg);
296         return $result;
297 }
298
299 /* 
300  *
301  * General function to send the push notification via cURL
302  *
303  */ 
304 function send_push($device_url, $headers, $msg) {
305         $ch = curl_init();
306         curl_setopt($ch, CURLOPT_URL, $device_url);
307         curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
308         curl_setopt($ch, CURLOPT_POST, true);
309         curl_setopt($ch, CURLOPT_HEADER, true); 
310         curl_setopt($ch, CURLOPT_HTTPHEADER,
311                 $headers + array(
312                         'Content-Type: text/xml',
313                         'charset=utf-8',
314                         'Accept: application/*',
315                         )
316                 );
317         curl_setopt($ch, CURLOPT_POSTFIELDS, $msg);
318
319         $output = curl_exec($ch);
320         curl_close($ch);
321
322         // if we received "Expired" from Microsoft server we will delete the obsolete device-URL
323         // and log this fact
324         $subscriptionStatus = get_header_value($output, 'X-SubscriptionStatus');
325         if ($subscriptionStatus == "Expired") {
326                 set_pconfig(local_user(),'windowsphonepush','device_url', "");
327                 logger("ERROR: the stored Device-URL " . $device_url . "returned an 'Expired' error, it has been deleted now.");
328         }
329
330         // the notification status shall be returned to windowsphonepush_cron (will 
331         // update settings if 'Received' otherwise keep old value in settings (on QueuedFull. Suppressed, N/A, Dropped)
332         $notificationStatus = get_header_value($output, 'X-NotificationStatus');
333         return $notificationStatus;
334     }
335
336 /*
337  * helper function to receive statuses from webresponse of Microsoft server
338  */ 
339 function get_header_value($content, $header) {
340         return preg_match_all("/$header: (.*)/i", $content, $match) ? $match[1][0] : "";
341 }
342
343
344 /*
345  * 
346  * reading information from url and deciding which function to start
347  * show_settings = delivering settings to check
348  * update_settings = set the device_url
349  * update_counterunseen = set counter for unseen elements to zero
350  *
351  */
352 function windowsphonepush_content(&$a) {        
353         // Login with the specified Network credentials (like in api.php)
354         windowsphonepush_login();
355
356         $path = $a->argv[0];
357         $path2 = $a->argv[1];
358         if ($path == "windowsphonepush") {
359                 switch ($path2) {
360                         case "show_settings":
361                                 windowsphonepush_showsettings($a);
362                                 killme();
363                                 break;
364                         case "update_settings":
365                                 $ret = windowsphonepush_updatesettings($a);
366                                 header("Content-Type: application/json; charset=utf-8");        
367                                 echo json_encode(array('status' => $ret));
368                                 killme();                               
369                                 break;
370                         case "update_counterunseen":
371                                 $ret = windowsphonepush_updatecounterunseen();
372                                 header("Content-Type: application/json; charset=utf-8");
373                                 echo json_encode(array('status' => $ret));
374                                 killme();
375                                 break;
376                         default:
377                                 echo "Fehler";
378                 }
379         }
380 }
381
382 /* 
383  * return settings for windowsphonepush addon to be able to check them in WP app
384  */
385 function windowsphonepush_showsettings(&$a) {
386         if(! local_user())
387                 return;
388
389         $enable = get_pconfig(local_user(), 'windowsphonepush', 'enable');
390         $device_url = get_pconfig(local_user(), 'windowsphonepush', 'device_url');
391         $senditemtext = get_pconfig(local_user(), 'windowsphonepush', 'senditemtext');
392         $lastpushid = get_pconfig(local_user(), 'windowsphonepush', 'lastpushid');
393         $counterunseen = get_pconfig(local_user(), 'windowsphonepush', 'counterunseen');
394         $addonversion = "2.0";
395
396         if (!$device_url)
397                 $device_url = "";
398
399         if (!$lastpushid)
400                 $lastpushid = 0;
401
402         header ("Content-Type: application/json");
403         echo json_encode(array('uid' => local_user(), 
404                                 'enable' => $enable, 
405                                 'device_url' => $device_url, 
406                                 'senditemtext' => $senditemtext,
407                                 'lastpushid' => $lastpushid, 
408                                 'counterunseen' => $counterunseen, 
409                                 'addonversion' => $addonversion));
410 }
411
412 /* 
413  * update_settings is used to transfer the device_url from WP device to the Friendica server
414  * return the status of the operation to the server
415  */
416 function windowsphonepush_updatesettings(&$a) {
417         if(! local_user()) {  
418                 return "Not Authenticated";
419         }
420
421         // no updating if user hasn't enabled the addon
422         $enable = PConfig::get(local_user(), 'windowsphonepush', 'enable');
423         if (!$enable) {
424                 return "Plug-in not enabled";
425         }
426
427         // check if sent url is empty - don't save and send return code to app
428         $device_url = $_POST['deviceurl'];
429         if ($device_url == "") {
430                 logger("ERROR: no valid Device-URL specified - client transferred '" . $device_url . "'");
431                 return "No valid Device-URL specified";
432         }
433
434         // check if sent url is already stored in database for another user, we assume that there was a change of 
435         // the user on the Windows Phone device and that device url is no longer true for the other user, so we
436         // et the device_url for the OTHER user blank (should normally not occur as App should include User/server 
437         // in url request to Microsoft Push Notification server)
438         $r = q("SELECT * FROM `pconfig` WHERE `uid` <> " . local_user() . " AND 
439                                                 `cat` = 'windowsphonepush' AND 
440                                                 `k` = 'device_url' AND 
441                                                 `v` = '" . $device_url . "'");
442         if(count($r)) {
443                 foreach($r as $rr) {
444                 set_pconfig($rr['uid'], 'windowsphonepush', 'device_url', '');
445                 logger("WARN: the sent URL was already registered with user '" . $rr['uid'] . "'. Deleted for this user as we expect to be correct now for user '" . local_user() . "'.");
446                 }
447         }
448
449         set_pconfig(local_user(),'windowsphonepush','device_url', $device_url);
450         // output the successfull update of the device URL to the logger for error analysis if necessary
451         logger("INFO: Device-URL for user '" . local_user() . "' has been updated with '" . $device_url . "'");
452         return "Device-URL updated successfully!";
453 }
454
455 /* 
456  * update_counterunseen is used to reset the counter to zero from Windows Phone app 
457  */
458 function windowsphonepush_updatecounterunseen() {
459         if(! local_user()) {  
460                 return "Not Authenticated";
461         }
462
463         // no updating if user hasn't enabled the addon
464         $enable = PConfig::get(local_user(), 'windowsphonepush', 'enable');
465         if (!$enable) {
466                 return "Plug-in not enabled";
467         }
468
469         set_pconfig(local_user(),'windowsphonepush','counterunseen', 0);
470         return "Counter set to zero";
471 }
472
473 /*
474  * helper function to login to the server with the specified Network credentials
475  * (mainly copied from api.php)
476  */
477 function windowsphonepush_login() {
478         if (!isset($_SERVER['PHP_AUTH_USER'])) {
479             logger('API_login: ' . print_r($_SERVER, true), LOGGER_DEBUG);
480             header('WWW-Authenticate: Basic realm="Friendica"');
481             header('HTTP/1.0 401 Unauthorized');
482             die('This api requires login');
483         }
484
485         $user = $_SERVER['PHP_AUTH_USER'];
486         $encrypted = hash('whirlpool',trim($_SERVER['PHP_AUTH_PW']));
487
488         // check if user specified by app is available in the user table
489         $r = q("SELECT * FROM `user` WHERE ( `email` = '%s' OR `nickname` = '%s' )
490             AND `password` = '%s' AND `blocked` = 0 AND `account_expired` = 0 AND `account_removed` = 0 AND `verified` = 1 LIMIT 1",
491             dbesc(trim($user)),
492             dbesc(trim($user)),
493             dbesc($encrypted)
494         );
495
496         if(count($r)){
497             $record = $r[0];
498         } else {
499             logger('API_login failure: ' . print_r($_SERVER,true), LOGGER_DEBUG);
500             header('WWW-Authenticate: Basic realm="Friendica"');
501             header('HTTP/1.0 401 Unauthorized');
502             die('This api requires login');
503         }
504
505         require_once 'include/security.php';
506         authenticate_success($record);
507         $_SESSION["allow_api"] = true;
508         Addon::callHooks('logged_in', $a->user);
509 }
510