db backup prior to drupal security update
[yaffs-website] / vendor / zendframework / zend-feed / src / PubSubHubbub / Subscriber.php
1 <?php
2 /**
3  * Zend Framework (http://framework.zend.com/)
4  *
5  * @link      http://github.com/zendframework/zf2 for the canonical source repository
6  * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7  * @license   http://framework.zend.com/license/new-bsd New BSD License
8  */
9
10 namespace Zend\Feed\PubSubHubbub;
11
12 use DateInterval;
13 use DateTime;
14 use Traversable;
15 use Zend\Feed\Uri;
16 use Zend\Http\Request as HttpRequest;
17 use Zend\Stdlib\ArrayUtils;
18
19 class Subscriber
20 {
21     /**
22      * An array of URLs for all Hub Servers to subscribe/unsubscribe.
23      *
24      * @var array
25      */
26     protected $hubUrls = [];
27
28     /**
29      * An array of optional parameters to be included in any
30      * (un)subscribe requests.
31      *
32      * @var array
33      */
34     protected $parameters = [];
35
36     /**
37      * The URL of the topic (Rss or Atom feed) which is the subject of
38      * our current intent to subscribe to/unsubscribe from updates from
39      * the currently configured Hub Servers.
40      *
41      * @var string
42      */
43     protected $topicUrl = '';
44
45     /**
46      * The URL Hub Servers must use when communicating with this Subscriber
47      *
48      * @var string
49      */
50     protected $callbackUrl = '';
51
52     /**
53      * The number of seconds for which the subscriber would like to have the
54      * subscription active. Defaults to null, i.e. not sent, to setup a
55      * permanent subscription if possible.
56      *
57      * @var int
58      */
59     protected $leaseSeconds = null;
60
61     /**
62      * The preferred verification mode (sync or async). By default, this
63      * Subscriber prefers synchronous verification, but is considered
64      * desirable to support asynchronous verification if possible.
65      *
66      * Zend\Feed\Pubsubhubbub\Subscriber will always send both modes, whose
67      * order of occurrence in the parameter list determines this preference.
68      *
69      * @var string
70      */
71     protected $preferredVerificationMode = PubSubHubbub::VERIFICATION_MODE_SYNC;
72
73     /**
74      * An array of any errors including keys for 'response', 'hubUrl'.
75      * The response is the actual Zend\Http\Response object.
76      *
77      * @var array
78      */
79     protected $errors = [];
80
81     /**
82      * An array of Hub Server URLs for Hubs operating at this time in
83      * asynchronous verification mode.
84      *
85      * @var array
86      */
87     protected $asyncHubs = [];
88
89     /**
90      * An instance of Zend\Feed\Pubsubhubbub\Model\SubscriptionPersistence used to background
91      * save any verification tokens associated with a subscription or other.
92      *
93      * @var \Zend\Feed\PubSubHubbub\Model\SubscriptionPersistenceInterface
94      */
95     protected $storage = null;
96
97     /**
98      * An array of authentication credentials for HTTP Basic Authentication
99      * if required by specific Hubs. The array is indexed by Hub Endpoint URI
100      * and the value is a simple array of the username and password to apply.
101      *
102      * @var array
103      */
104     protected $authentications = [];
105
106     /**
107      * Tells the Subscriber to append any subscription identifier to the path
108      * of the base Callback URL. E.g. an identifier "subkey1" would be added
109      * to the callback URL "http://www.example.com/callback" to create a subscription
110      * specific Callback URL of "http://www.example.com/callback/subkey1".
111      *
112      * This is required for all Hubs using the Pubsubhubbub 0.1 Specification.
113      * It should be manually intercepted and passed to the Callback class using
114      * Zend\Feed\Pubsubhubbub\Subscriber\Callback::setSubscriptionKey(). Will
115      * require a route in the form "callback/:subkey" to allow the parameter be
116      * retrieved from an action using the Zend\Controller\Action::\getParam()
117      * method.
118      *
119      * @var string
120      */
121     protected $usePathParameter = false;
122
123     /**
124      * Constructor; accepts an array or Traversable instance to preset
125      * options for the Subscriber without calling all supported setter
126      * methods in turn.
127      *
128      * @param  array|Traversable $options
129      */
130     public function __construct($options = null)
131     {
132         if ($options !== null) {
133             $this->setOptions($options);
134         }
135     }
136
137     /**
138      * Process any injected configuration options
139      *
140      * @param  array|Traversable $options
141      * @return Subscriber
142      * @throws Exception\InvalidArgumentException
143      */
144     public function setOptions($options)
145     {
146         if ($options instanceof Traversable) {
147             $options = ArrayUtils::iteratorToArray($options);
148         }
149
150         if (! is_array($options)) {
151             throw new Exception\InvalidArgumentException('Array or Traversable object'
152                                 . 'expected, got ' . gettype($options));
153         }
154         if (array_key_exists('hubUrls', $options)) {
155             $this->addHubUrls($options['hubUrls']);
156         }
157         if (array_key_exists('callbackUrl', $options)) {
158             $this->setCallbackUrl($options['callbackUrl']);
159         }
160         if (array_key_exists('topicUrl', $options)) {
161             $this->setTopicUrl($options['topicUrl']);
162         }
163         if (array_key_exists('storage', $options)) {
164             $this->setStorage($options['storage']);
165         }
166         if (array_key_exists('leaseSeconds', $options)) {
167             $this->setLeaseSeconds($options['leaseSeconds']);
168         }
169         if (array_key_exists('parameters', $options)) {
170             $this->setParameters($options['parameters']);
171         }
172         if (array_key_exists('authentications', $options)) {
173             $this->addAuthentications($options['authentications']);
174         }
175         if (array_key_exists('usePathParameter', $options)) {
176             $this->usePathParameter($options['usePathParameter']);
177         }
178         if (array_key_exists('preferredVerificationMode', $options)) {
179             $this->setPreferredVerificationMode(
180                 $options['preferredVerificationMode']
181             );
182         }
183         return $this;
184     }
185
186     /**
187      * Set the topic URL (RSS or Atom feed) to which the intended (un)subscribe
188      * event will relate
189      *
190      * @param  string $url
191      * @return Subscriber
192      * @throws Exception\InvalidArgumentException
193      */
194     public function setTopicUrl($url)
195     {
196         if (empty($url) || ! is_string($url) || ! Uri::factory($url)->isValid()) {
197             throw new Exception\InvalidArgumentException('Invalid parameter "url"'
198                 .' of "' . $url . '" must be a non-empty string and a valid'
199                 .' URL');
200         }
201         $this->topicUrl = $url;
202         return $this;
203     }
204
205     /**
206      * Set the topic URL (RSS or Atom feed) to which the intended (un)subscribe
207      * event will relate
208      *
209      * @return string
210      * @throws Exception\RuntimeException
211      */
212     public function getTopicUrl()
213     {
214         if (empty($this->topicUrl)) {
215             throw new Exception\RuntimeException('A valid Topic (RSS or Atom'
216                 . ' feed) URL MUST be set before attempting any operation');
217         }
218         return $this->topicUrl;
219     }
220
221     /**
222      * Set the number of seconds for which any subscription will remain valid
223      *
224      * @param  int $seconds
225      * @return Subscriber
226      * @throws Exception\InvalidArgumentException
227      */
228     public function setLeaseSeconds($seconds)
229     {
230         $seconds = intval($seconds);
231         if ($seconds <= 0) {
232             throw new Exception\InvalidArgumentException('Expected lease seconds'
233                 . ' must be an integer greater than zero');
234         }
235         $this->leaseSeconds = $seconds;
236         return $this;
237     }
238
239     /**
240      * Get the number of lease seconds on subscriptions
241      *
242      * @return int
243      */
244     public function getLeaseSeconds()
245     {
246         return $this->leaseSeconds;
247     }
248
249     /**
250      * Set the callback URL to be used by Hub Servers when communicating with
251      * this Subscriber
252      *
253      * @param  string $url
254      * @return Subscriber
255      * @throws Exception\InvalidArgumentException
256      */
257     public function setCallbackUrl($url)
258     {
259         if (empty($url) || ! is_string($url) || ! Uri::factory($url)->isValid()) {
260             throw new Exception\InvalidArgumentException('Invalid parameter "url"'
261                 . ' of "' . $url . '" must be a non-empty string and a valid'
262                 . ' URL');
263         }
264         $this->callbackUrl = $url;
265         return $this;
266     }
267
268     /**
269      * Get the callback URL to be used by Hub Servers when communicating with
270      * this Subscriber
271      *
272      * @return string
273      * @throws Exception\RuntimeException
274      */
275     public function getCallbackUrl()
276     {
277         if (empty($this->callbackUrl)) {
278             throw new Exception\RuntimeException('A valid Callback URL MUST be'
279                 . ' set before attempting any operation');
280         }
281         return $this->callbackUrl;
282     }
283
284     /**
285      * Set preferred verification mode (sync or async). By default, this
286      * Subscriber prefers synchronous verification, but does support
287      * asynchronous if that's the Hub Server's utilised mode.
288      *
289      * Zend\Feed\Pubsubhubbub\Subscriber will always send both modes, whose
290      * order of occurrence in the parameter list determines this preference.
291      *
292      * @param  string $mode Should be 'sync' or 'async'
293      * @return Subscriber
294      * @throws Exception\InvalidArgumentException
295      */
296     public function setPreferredVerificationMode($mode)
297     {
298         if ($mode !== PubSubHubbub::VERIFICATION_MODE_SYNC
299             && $mode !== PubSubHubbub::VERIFICATION_MODE_ASYNC
300         ) {
301             throw new Exception\InvalidArgumentException('Invalid preferred'
302                 . ' mode specified: "' . $mode . '" but should be one of'
303                 . ' Zend\Feed\Pubsubhubbub::VERIFICATION_MODE_SYNC or'
304                 . ' Zend\Feed\Pubsubhubbub::VERIFICATION_MODE_ASYNC');
305         }
306         $this->preferredVerificationMode = $mode;
307         return $this;
308     }
309
310     /**
311      * Get preferred verification mode (sync or async).
312      *
313      * @return string
314      */
315     public function getPreferredVerificationMode()
316     {
317         return $this->preferredVerificationMode;
318     }
319
320     /**
321      * Add a Hub Server URL supported by Publisher
322      *
323      * @param  string $url
324      * @return Subscriber
325      * @throws Exception\InvalidArgumentException
326      */
327     public function addHubUrl($url)
328     {
329         if (empty($url) || ! is_string($url) || ! Uri::factory($url)->isValid()) {
330             throw new Exception\InvalidArgumentException('Invalid parameter "url"'
331                 . ' of "' . $url . '" must be a non-empty string and a valid'
332                 . ' URL');
333         }
334         $this->hubUrls[] = $url;
335         return $this;
336     }
337
338     /**
339      * Add an array of Hub Server URLs supported by Publisher
340      *
341      * @param  array $urls
342      * @return Subscriber
343      */
344     public function addHubUrls(array $urls)
345     {
346         foreach ($urls as $url) {
347             $this->addHubUrl($url);
348         }
349         return $this;
350     }
351
352     /**
353      * Remove a Hub Server URL
354      *
355      * @param  string $url
356      * @return Subscriber
357      */
358     public function removeHubUrl($url)
359     {
360         if (! in_array($url, $this->getHubUrls())) {
361             return $this;
362         }
363         $key = array_search($url, $this->hubUrls);
364         unset($this->hubUrls[$key]);
365         return $this;
366     }
367
368     /**
369      * Return an array of unique Hub Server URLs currently available
370      *
371      * @return array
372      */
373     public function getHubUrls()
374     {
375         $this->hubUrls = array_unique($this->hubUrls);
376         return $this->hubUrls;
377     }
378
379     /**
380      * Add authentication credentials for a given URL
381      *
382      * @param  string $url
383      * @param  array $authentication
384      * @return Subscriber
385      * @throws Exception\InvalidArgumentException
386      */
387     public function addAuthentication($url, array $authentication)
388     {
389         if (empty($url) || ! is_string($url) || ! Uri::factory($url)->isValid()) {
390             throw new Exception\InvalidArgumentException('Invalid parameter "url"'
391                 . ' of "' . $url . '" must be a non-empty string and a valid'
392                 . ' URL');
393         }
394         $this->authentications[$url] = $authentication;
395         return $this;
396     }
397
398     /**
399      * Add authentication credentials for hub URLs
400      *
401      * @param  array $authentications
402      * @return Subscriber
403      */
404     public function addAuthentications(array $authentications)
405     {
406         foreach ($authentications as $url => $authentication) {
407             $this->addAuthentication($url, $authentication);
408         }
409         return $this;
410     }
411
412     /**
413      * Get all hub URL authentication credentials
414      *
415      * @return array
416      */
417     public function getAuthentications()
418     {
419         return $this->authentications;
420     }
421
422     /**
423      * Set flag indicating whether or not to use a path parameter
424      *
425      * @param  bool $bool
426      * @return Subscriber
427      */
428     public function usePathParameter($bool = true)
429     {
430         $this->usePathParameter = $bool;
431         return $this;
432     }
433
434     /**
435      * Add an optional parameter to the (un)subscribe requests
436      *
437      * @param  string $name
438      * @param  string|null $value
439      * @return Subscriber
440      * @throws Exception\InvalidArgumentException
441      */
442     public function setParameter($name, $value = null)
443     {
444         if (is_array($name)) {
445             $this->setParameters($name);
446             return $this;
447         }
448         if (empty($name) || ! is_string($name)) {
449             throw new Exception\InvalidArgumentException('Invalid parameter "name"'
450                 . ' of "' . $name . '" must be a non-empty string');
451         }
452         if ($value === null) {
453             $this->removeParameter($name);
454             return $this;
455         }
456         if (empty($value) || (! is_string($value) && $value !== null)) {
457             throw new Exception\InvalidArgumentException('Invalid parameter "value"'
458                 . ' of "' . $value . '" must be a non-empty string');
459         }
460         $this->parameters[$name] = $value;
461         return $this;
462     }
463
464     /**
465      * Add an optional parameter to the (un)subscribe requests
466      *
467      * @param  array $parameters
468      * @return Subscriber
469      */
470     public function setParameters(array $parameters)
471     {
472         foreach ($parameters as $name => $value) {
473             $this->setParameter($name, $value);
474         }
475         return $this;
476     }
477
478     /**
479      * Remove an optional parameter for the (un)subscribe requests
480      *
481      * @param  string $name
482      * @return Subscriber
483      * @throws Exception\InvalidArgumentException
484      */
485     public function removeParameter($name)
486     {
487         if (empty($name) || ! is_string($name)) {
488             throw new Exception\InvalidArgumentException('Invalid parameter "name"'
489                 . ' of "' . $name . '" must be a non-empty string');
490         }
491         if (array_key_exists($name, $this->parameters)) {
492             unset($this->parameters[$name]);
493         }
494         return $this;
495     }
496
497     /**
498      * Return an array of optional parameters for (un)subscribe requests
499      *
500      * @return array
501      */
502     public function getParameters()
503     {
504         return $this->parameters;
505     }
506
507     /**
508      * Sets an instance of Zend\Feed\Pubsubhubbub\Model\SubscriptionPersistence used to background
509      * save any verification tokens associated with a subscription or other.
510      *
511      * @param  Model\SubscriptionPersistenceInterface $storage
512      * @return Subscriber
513      */
514     public function setStorage(Model\SubscriptionPersistenceInterface $storage)
515     {
516         $this->storage = $storage;
517         return $this;
518     }
519
520     /**
521      * Gets an instance of Zend\Feed\Pubsubhubbub\Storage\StoragePersistence used
522      * to background save any verification tokens associated with a subscription
523      * or other.
524      *
525      * @return Model\SubscriptionPersistenceInterface
526      * @throws Exception\RuntimeException
527      */
528     public function getStorage()
529     {
530         if ($this->storage === null) {
531             throw new Exception\RuntimeException('No storage vehicle '
532                 . 'has been set.');
533         }
534         return $this->storage;
535     }
536
537     /**
538      * Subscribe to one or more Hub Servers using the stored Hub URLs
539      * for the given Topic URL (RSS or Atom feed)
540      *
541      * @return void
542      */
543     public function subscribeAll()
544     {
545         $this->_doRequest('subscribe');
546     }
547
548     /**
549      * Unsubscribe from one or more Hub Servers using the stored Hub URLs
550      * for the given Topic URL (RSS or Atom feed)
551      *
552      * @return void
553      */
554     public function unsubscribeAll()
555     {
556         $this->_doRequest('unsubscribe');
557     }
558
559     /**
560      * Returns a boolean indicator of whether the notifications to Hub
561      * Servers were ALL successful. If even one failed, FALSE is returned.
562      *
563      * @return bool
564      */
565     public function isSuccess()
566     {
567         if (count($this->errors) > 0) {
568             return false;
569         }
570         return true;
571     }
572
573     /**
574      * Return an array of errors met from any failures, including keys:
575      * 'response' => the Zend\Http\Response object from the failure
576      * 'hubUrl' => the URL of the Hub Server whose notification failed
577      *
578      * @return array
579      */
580     public function getErrors()
581     {
582         return $this->errors;
583     }
584
585     /**
586      * Return an array of Hub Server URLs who returned a response indicating
587      * operation in Asynchronous Verification Mode, i.e. they will not confirm
588      * any (un)subscription immediately but at a later time (Hubs may be
589      * doing this as a batch process when load balancing)
590      *
591      * @return array
592      */
593     public function getAsyncHubs()
594     {
595         return $this->asyncHubs;
596     }
597
598     /**
599      * Executes an (un)subscribe request
600      *
601      * @param  string $mode
602      * @return void
603      * @throws Exception\RuntimeException
604      */
605     // @codingStandardsIgnoreStart
606     protected function _doRequest($mode)
607     {
608         // @codingStandardsIgnoreEnd
609         $client = $this->_getHttpClient();
610         $hubs   = $this->getHubUrls();
611         if (empty($hubs)) {
612             throw new Exception\RuntimeException('No Hub Server URLs'
613                 . ' have been set so no subscriptions can be attempted');
614         }
615         $this->errors = [];
616         $this->asyncHubs = [];
617         foreach ($hubs as $url) {
618             if (array_key_exists($url, $this->authentications)) {
619                 $auth = $this->authentications[$url];
620                 $client->setAuth($auth[0], $auth[1]);
621             }
622             $client->setUri($url);
623             $client->setRawBody($params = $this->_getRequestParameters($url, $mode));
624             $response = $client->send();
625             if ($response->getStatusCode() !== 204
626                 && $response->getStatusCode() !== 202
627             ) {
628                 $this->errors[] = [
629                     'response' => $response,
630                     'hubUrl'   => $url,
631                 ];
632             /**
633              * At first I thought it was needed, but the backend storage will
634              * allow tracking async without any user interference. It's left
635              * here in case the user is interested in knowing what Hubs
636              * are using async verification modes so they may update Models and
637              * move these to asynchronous processes.
638              */
639             } elseif ($response->getStatusCode() == 202) {
640                 $this->asyncHubs[] = [
641                     'response' => $response,
642                     'hubUrl'   => $url,
643                 ];
644             }
645         }
646     }
647
648     /**
649      * Get a basic prepared HTTP client for use
650      *
651      * @return \Zend\Http\Client
652      */
653     // @codingStandardsIgnoreStart
654     protected function _getHttpClient()
655     {
656         // @codingStandardsIgnoreEnd
657         $client = PubSubHubbub::getHttpClient();
658         $client->setMethod(HttpRequest::METHOD_POST);
659         $client->setOptions(['useragent' => 'Zend_Feed_Pubsubhubbub_Subscriber/'
660             . Version::VERSION]);
661         return $client;
662     }
663
664     /**
665      * Return a list of standard protocol/optional parameters for addition to
666      * client's POST body that are specific to the current Hub Server URL
667      *
668      * @param  string $hubUrl
669      * @param  string $mode
670      * @return string
671      * @throws Exception\InvalidArgumentException
672      */
673     // @codingStandardsIgnoreStart
674     protected function _getRequestParameters($hubUrl, $mode)
675     {
676         // @codingStandardsIgnoreEnd
677         if (! in_array($mode, ['subscribe', 'unsubscribe'])) {
678             throw new Exception\InvalidArgumentException('Invalid mode specified: "'
679                 . $mode . '" which should have been "subscribe" or "unsubscribe"');
680         }
681
682         $params = [
683             'hub.mode'  => $mode,
684             'hub.topic' => $this->getTopicUrl(),
685         ];
686
687         if ($this->getPreferredVerificationMode()
688                 == PubSubHubbub::VERIFICATION_MODE_SYNC
689         ) {
690             $vmodes = [
691                 PubSubHubbub::VERIFICATION_MODE_SYNC,
692                 PubSubHubbub::VERIFICATION_MODE_ASYNC,
693             ];
694         } else {
695             $vmodes = [
696                 PubSubHubbub::VERIFICATION_MODE_ASYNC,
697                 PubSubHubbub::VERIFICATION_MODE_SYNC,
698             ];
699         }
700         $params['hub.verify'] = [];
701         foreach ($vmodes as $vmode) {
702             $params['hub.verify'][] = $vmode;
703         }
704
705         /**
706          * Establish a persistent verify_token and attach key to callback
707          * URL's path/query_string
708          */
709         $key   = $this->_generateSubscriptionKey($params, $hubUrl);
710         $token = $this->_generateVerifyToken();
711         $params['hub.verify_token'] = $token;
712
713         // Note: query string only usable with PuSH 0.2 Hubs
714         if (! $this->usePathParameter) {
715             $params['hub.callback'] = $this->getCallbackUrl()
716                 . '?xhub.subscription=' . PubSubHubbub::urlencode($key);
717         } else {
718             $params['hub.callback'] = rtrim($this->getCallbackUrl(), '/')
719                 . '/' . PubSubHubbub::urlencode($key);
720         }
721         if ($mode == 'subscribe' && $this->getLeaseSeconds() !== null) {
722             $params['hub.lease_seconds'] = $this->getLeaseSeconds();
723         }
724
725         // hub.secret not currently supported
726         $optParams = $this->getParameters();
727         foreach ($optParams as $name => $value) {
728             $params[$name] = $value;
729         }
730
731         // store subscription to storage
732         $now = new DateTime();
733         $expires = null;
734         if (isset($params['hub.lease_seconds'])) {
735             $expires = $now->add(new DateInterval('PT' . $params['hub.lease_seconds'] . 'S'))
736                 ->format('Y-m-d H:i:s');
737         }
738         $data = [
739             'id'                 => $key,
740             'topic_url'          => $params['hub.topic'],
741             'hub_url'            => $hubUrl,
742             'created_time'       => $now->format('Y-m-d H:i:s'),
743             'lease_seconds'      => $params['hub.lease_seconds'],
744             'verify_token'       => hash('sha256', $params['hub.verify_token']),
745             'secret'             => null,
746             'expiration_time'    => $expires,
747             // @codingStandardsIgnoreStart
748             'subscription_state' => ($mode == 'unsubscribe') ? PubSubHubbub::SUBSCRIPTION_TODELETE : PubSubHubbub::SUBSCRIPTION_NOTVERIFIED,
749             // @codingStandardsIgnoreEnd
750         ];
751         $this->getStorage()->setSubscription($data);
752
753         return $this->_toByteValueOrderedString(
754             $this->_urlEncode($params)
755         );
756     }
757
758     /**
759      * Simple helper to generate a verification token used in (un)subscribe
760      * requests to a Hub Server. Follows no particular method, which means
761      * it might be improved/changed in future.
762      *
763      * @return string
764      */
765     // @codingStandardsIgnoreStart
766     protected function _generateVerifyToken()
767     {
768         // @codingStandardsIgnoreEnd
769         if (! empty($this->testStaticToken)) {
770             return $this->testStaticToken;
771         }
772         return uniqid(rand(), true) . time();
773     }
774
775     /**
776      * Simple helper to generate a verification token used in (un)subscribe
777      * requests to a Hub Server.
778      *
779      * @param array   $params
780      * @param string $hubUrl The Hub Server URL for which this token will apply
781      * @return string
782      */
783     // @codingStandardsIgnoreStart
784     protected function _generateSubscriptionKey(array $params, $hubUrl)
785     {
786         // @codingStandardsIgnoreEnd
787         $keyBase = $params['hub.topic'] . $hubUrl;
788         $key     = md5($keyBase);
789
790         return $key;
791     }
792
793     /**
794      * URL Encode an array of parameters
795      *
796      * @param  array $params
797      * @return array
798      */
799     // @codingStandardsIgnoreStart
800     protected function _urlEncode(array $params)
801     {
802         // @codingStandardsIgnoreEnd
803         $encoded = [];
804         foreach ($params as $key => $value) {
805             if (is_array($value)) {
806                 $ekey = PubSubHubbub::urlencode($key);
807                 $encoded[$ekey] = [];
808                 foreach ($value as $duplicateKey) {
809                     $encoded[$ekey][]
810                         = PubSubHubbub::urlencode($duplicateKey);
811                 }
812             } else {
813                 $encoded[PubSubHubbub::urlencode($key)]
814                     = PubSubHubbub::urlencode($value);
815             }
816         }
817         return $encoded;
818     }
819
820     /**
821      * Order outgoing parameters
822      *
823      * @param  array $params
824      * @return array
825      */
826     // @codingStandardsIgnoreStart
827     protected function _toByteValueOrderedString(array $params)
828     {
829         // @codingStandardsIgnoreEnd
830         $return = [];
831         uksort($params, 'strnatcmp');
832         foreach ($params as $key => $value) {
833             if (is_array($value)) {
834                 foreach ($value as $keyduplicate) {
835                     $return[] = $key . '=' . $keyduplicate;
836                 }
837             } else {
838                 $return[] = $key . '=' . $value;
839             }
840         }
841         return implode('&', $return);
842     }
843
844     /**
845      * This is STRICTLY for testing purposes only...
846      */
847     protected $testStaticToken = null;
848
849     final public function setTestStaticToken($token)
850     {
851         $this->testStaticToken = (string) $token;
852     }
853 }