1
|
|
|
<?php |
2
|
|
|
namespace PHPDaemon\Clients\XMPP; |
3
|
|
|
|
4
|
|
|
use PHPDaemon\Traits\EventHandlers; |
5
|
|
|
|
6
|
|
|
class XMPPRoster |
7
|
|
|
{ |
8
|
|
|
use EventHandlers; |
9
|
|
|
use \PHPDaemon\Traits\ClassWatchdog; |
10
|
|
|
use \PHPDaemon\Traits\StaticObjectWatchdog; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* @var Connection |
14
|
|
|
*/ |
15
|
|
|
public $xmpp; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* @var array |
19
|
|
|
*/ |
20
|
|
|
public $roster_array = []; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var boolean |
24
|
|
|
*/ |
25
|
|
|
public $track_presence = true; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var boolean |
29
|
|
|
*/ |
30
|
|
|
public $auto_subscribe = true; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @var string |
34
|
|
|
*/ |
35
|
|
|
public $ns = 'jabber:iq:roster'; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Constructor |
39
|
|
|
* @param Connection $xmpp |
40
|
|
|
*/ |
41
|
|
|
public function __construct($xmpp) |
42
|
|
|
{ |
43
|
|
|
$this->xmpp = $xmpp; |
44
|
|
|
|
45
|
|
|
$this->xmpp->xml->addXPathHandler('{jabber:client}presence', function ($xml) { |
46
|
|
|
$payload = []; |
47
|
|
|
$payload['type'] = (isset($xml->attrs['type'])) ? $xml->attrs['type'] : 'available'; |
48
|
|
|
$payload['show'] = (isset($xml->sub('show')->data)) ? $xml->sub('show')->data : $payload['type']; |
49
|
|
|
$payload['from'] = $xml->attrs['from']; |
50
|
|
|
$payload['status'] = (isset($xml->sub('status')->data)) ? $xml->sub('status')->data : ''; |
51
|
|
|
$payload['priority'] = (isset($xml->sub('priority')->data)) ? intval($xml->sub('priority')->data) : 0; |
52
|
|
|
$payload['xml'] = $xml; |
53
|
|
|
if (($payload['from'] === $this->xmpp->fulljid) && $payload['type'] === 'unavailable') { |
54
|
|
|
$this->xmpp->finish(); |
55
|
|
|
} |
56
|
|
|
if ($this->track_presence) { |
57
|
|
|
$this->setPresence($payload['from'], $payload['priority'], $payload['show'], $payload['status']); |
58
|
|
|
} |
59
|
|
|
//Daemon::log("Presence: {$payload['from']} [{$payload['show']}] {$payload['status']}"); |
|
|
|
|
60
|
|
|
if (array_key_exists('type', $xml->attrs) and $xml->attrs['type'] === 'subscribe') { |
|
|
|
|
61
|
|
|
if ($this->auto_subscribe) { |
62
|
|
|
$this->xmpp->sendXML("<presence type='subscribed' to='{$xml->attrs['from']}' from='{$this->xmpp->fulljid}' />"); |
|
|
|
|
63
|
|
|
$this->xmpp->sendXML("<presence type='subscribe' to='{$xml->attrs['from']}' from='{$this->xmpp->fulljid}' />"); |
|
|
|
|
64
|
|
|
} |
65
|
|
|
$this->event('subscription_requested', $payload); |
66
|
|
|
} elseif (array_key_exists('type', $xml->attrs) and $xml->attrs['type'] === 'subscribed') { |
|
|
|
|
67
|
|
|
$this->event('subscription_accepted', $payload); |
68
|
|
|
} else { |
69
|
|
|
$this->event('presence', $payload); |
70
|
|
|
} |
71
|
|
|
}); |
72
|
|
|
$this->fetch(); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @TODO |
|
|
|
|
77
|
|
|
* @param string $xml |
78
|
|
|
* @param callable $cb |
|
|
|
|
79
|
|
|
* @callback $cb ( ) |
80
|
|
|
*/ |
81
|
|
|
public function rosterSet($xml, $cb = null) |
82
|
|
|
{ |
83
|
|
|
$this->xmpp->querySetTo($this->xmpp->fulljid, $this->ns, $xml, $cb); |
|
|
|
|
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @TODO |
|
|
|
|
88
|
|
|
* @param string $jid |
89
|
|
|
* @param string $type |
90
|
|
|
* @param callable $cb |
|
|
|
|
91
|
|
|
* @callback $cb ( ) |
92
|
|
|
*/ |
93
|
|
|
public function setSubscription($jid, $type, $cb = null) |
94
|
|
|
{ |
95
|
|
|
$this->rosterSet('<item jid="' . htmlspecialchars($jid) . '" subscription="' . htmlspecialchars($type) . '" />', $cb); |
|
|
|
|
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* @TODO |
|
|
|
|
100
|
|
|
* @param callable $cb |
|
|
|
|
101
|
|
|
* @callback $cb ( ) |
102
|
|
|
*/ |
103
|
|
|
public function fetch($cb = null) |
104
|
|
|
{ |
105
|
|
|
$this->xmpp->queryGet($this->ns, function ($xml) use ($cb) { |
106
|
|
|
$status = "result"; |
107
|
|
|
$xmlroster = $xml->sub('query'); |
108
|
|
|
$contacts = []; |
109
|
|
|
foreach ($xmlroster->subs as $item) { |
110
|
|
|
$groups = []; |
111
|
|
|
if ($item->name === 'item') { |
112
|
|
|
$jid = $item->attrs['jid']; //REQUIRED |
113
|
|
|
$name = isset($item->attrs['name']) ? $item->attrs['name'] : ''; //MAY |
114
|
|
|
$subscription = $item->attrs['subscription']; |
115
|
|
|
foreach ($item->subs as $subitem) { |
116
|
|
|
if ($subitem->name === 'group') { |
117
|
|
|
$groups[] = $subitem->data; |
118
|
|
|
} |
119
|
|
|
} |
120
|
|
|
$contacts[] = [$jid, $subscription, $name, $groups]; //Store for action if no errors happen |
121
|
|
|
} else { |
122
|
|
|
$status = 'error'; |
123
|
|
|
} |
124
|
|
|
} |
125
|
|
|
if ($status === 'result') { //No errors, add contacts |
126
|
|
|
foreach ($contacts as $contact) { |
127
|
|
|
$this->_addContact($contact[0], $contact[1], $contact[2], $contact[3]); |
128
|
|
|
} |
129
|
|
|
} |
130
|
|
|
if ($xml->attrs['type'] === 'set') { |
131
|
|
|
$this->xmpp->sendXML('<iq type="reply" id="' . $xml->attrs['id'] . '" to="' . $xml->attrs['from'] . '" />'); |
|
|
|
|
132
|
|
|
} |
133
|
|
|
if ($cb) { |
134
|
|
|
$cb($status); |
135
|
|
|
} |
136
|
|
|
}); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Add given contact to roster |
141
|
|
|
* @param string $jid |
142
|
|
|
* @param string $subscription |
143
|
|
|
* @param string $name |
144
|
|
|
* @param array $groups |
145
|
|
|
*/ |
146
|
|
|
public function _addContact($jid, $subscription, $name = '', $groups = []) |
147
|
|
|
{ |
148
|
|
|
$contact = ['jid' => $jid, 'subscription' => $subscription, 'name' => $name, 'groups' => $groups]; |
149
|
|
|
if ($this->isContact($jid)) { |
150
|
|
|
$this->roster_array[$jid]['contact'] = $contact; |
151
|
|
|
} else { |
152
|
|
|
$this->roster_array[$jid] = ['contact' => $contact]; |
153
|
|
|
} |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Retrieve contact via jid |
158
|
|
|
* @param string $jid |
159
|
|
|
* @return array|null |
160
|
|
|
*/ |
161
|
|
|
public function getContact($jid) |
162
|
|
|
{ |
163
|
|
|
if ($this->isContact($jid)) { |
164
|
|
|
return $this->roster_array[$jid]['contact']; |
165
|
|
|
} |
166
|
|
|
return null; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* Discover if a contact exists in the roster via jid |
171
|
|
|
* @param string $jid |
172
|
|
|
* @return boolean |
173
|
|
|
*/ |
174
|
|
|
public function isContact($jid) |
175
|
|
|
{ |
176
|
|
|
return array_key_exists($jid, $this->roster_array); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Set presence |
181
|
|
|
* @param string $presence |
182
|
|
|
* @param integer $priority |
183
|
|
|
* @param string $show |
184
|
|
|
* @param string $status |
185
|
|
|
*/ |
186
|
|
|
public function setPresence($presence, $priority, $show, $status) |
187
|
|
|
{ |
188
|
|
|
list($jid, $resource) = explode('/', $presence . '/'); |
189
|
|
|
if ($show !== 'unavailable') { |
190
|
|
|
if (!$this->isContact($jid)) { |
191
|
|
|
$this->_addContact($jid, 'not-in-roster'); |
192
|
|
|
} |
193
|
|
|
$this->roster_array[$jid]['presence'][$resource] = ['priority' => $priority, 'show' => $show, 'status' => $status]; |
|
|
|
|
194
|
|
|
} else { //Nuke unavailable resources to save memory |
195
|
|
|
unset($this->roster_array[$jid]['resource'][$resource]); |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Return best presence for jid |
201
|
|
|
* @param string $jid |
202
|
|
|
* @return array|false |
203
|
|
|
*/ |
204
|
|
|
public function getPresence($jid) |
205
|
|
|
{ |
206
|
|
|
$split = split("/", $jid); |
207
|
|
|
$jid = $split[0]; |
208
|
|
|
if (!$this->isContact($jid)) { |
209
|
|
|
return false; |
210
|
|
|
} |
211
|
|
|
$current = ['resource' => '', 'active' => '', 'priority' => -129, 'show' => '', 'status' => '']; //Priorities can only be -128 = 127 |
|
|
|
|
212
|
|
|
foreach ($this->roster_array[$jid]['presence'] as $resource => $presence) { |
213
|
|
|
//Highest available priority or just highest priority |
214
|
|
|
if ($presence['priority'] > $current['priority'] && (($presence['show'] === 'chat' || $presence['show'] === 'available') or ($current['show'] !== 'chat' or $current['show'] !== 'available'))) { |
|
|
|
|
215
|
|
|
$current = $presence; |
216
|
|
|
$current['resource'] = $resource; |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
return $current; |
220
|
|
|
} |
221
|
|
|
} |
222
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.