File: Utils.php

package info (click to toggle)
php-horde-routes 2.0.5-9
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, sid
  • size: 544 kB
  • sloc: php: 3,653; xml: 360; sh: 3; makefile: 2
file content (444 lines) | stat: -rw-r--r-- 16,057 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
<?php
/**
 * Horde Routes package
 *
 * This package is heavily inspired by the Python "Routes" library
 * by Ben Bangert (http://routes.groovie.org).  Routes is based
 * largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
 *
 * @author  Maintainable Software, LLC. (http://www.maintainable.com)
 * @author  Mike Naberezny <mike@maintainable.com>
 * @license http://www.horde.org/licenses/bsd BSD
 * @package Routes
 */

/**
 * Utility functions for use in templates and controllers
 *
 * @package Routes
 */
class Horde_Routes_Utils
{
    /**
     * @var Horde_Routes_Mapper
     */
    public $mapper;

    /**
     * Match data from last match; implements for urlFor() route memory
     * @var array
     */
    public $mapperDict = array();

    /**
     * Callback function used for redirectTo()
     * @var callback
     */
    public $redirect;

    /**
     * Constructor
     *
     * @param  Horde_Routes_Mapper  $mapper    Mapper for these utilities
     * @param  callback             $redirect  Redirect callback for redirectTo()
     */
    public function __construct(Horde_Routes_Mapper $mapper, $redirect = null)
    {
        $this->mapper   = $mapper;
        $this->redirect = $redirect;
    }

    /**
     * Generates a URL.
     *
     * All keys given to urlFor are sent to the Routes Mapper instance for
     * generation except for::
     *
     *     anchor          specified the anchor name to be appened to the path
     *     host            overrides the default (current) host if provided
     *     protocol        overrides the default (current) protocol if provided
     *     qualified       creates the URL with the host/port information as
     *                     needed
     *
     * The URL is generated based on the rest of the keys. When generating a new
     * URL, values will be used from the current request's parameters (if
     * present). The following rules are used to determine when and how to keep
     * the current requests parameters:
     *
     * * If the controller is present and begins with '/', no defaults are used
     * * If the controller is changed, action is set to 'index' unless otherwise
     *   specified
     *
     * For example, if the current request yielded a dict (associative array) of
     * array('controller'=>'blog', 'action'=>'view', 'id'=>2), with the standard
     * ':controller/:action/:id' route, you'd get the following results::
     *
     *     urlFor(array('id'=>4))                    =>  '/blog/view/4',
     *     urlFor(array('controller'=>'/admin'))     =>  '/admin',
     *     urlFor(array('controller'=>'admin'))      =>  '/admin/view/2'
     *     urlFor(array('action'=>'edit'))           =>  '/blog/edit/2',
     *     urlFor(array('action'=>'list', id=NULL))  =>  '/blog/list'
     *
     * **Static and Named Routes**
     *
     * If there is a string present as the first argument, a lookup is done
     * against the named routes table to see if there's any matching routes. The
     * keyword defaults used with static routes will be sent in as GET query
     * arg's if a route matches.
     *
     * If no route by that name is found, the string is assumed to be a raw URL.
     * Should the raw URL begin with ``/`` then appropriate SCRIPT_NAME data will
     * be added if present, otherwise the string will be used as the url with
     * keyword args becoming GET query args.
     */
    public function urlFor($first = array(), $second = array())
    {
        if (is_array($first)) {
            // urlFor(array('controller' => 'foo', ...))
            $routeName = null;
            $kargs = $first;
        } else {
            // urlFor('named_route')
            // urlFor('named_route', array('id' => 3, ...))
            // urlFor('static_path')
            $routeName = (string)$first;
            $kargs = $second;
        }

        $anchor    = isset($kargs['anchor'])    ? $kargs['anchor']    : null;
        $host      = isset($kargs['host'])      ? $kargs['host']      : null;
        $protocol  = isset($kargs['protocol'])  ? $kargs['protocol']  : null;
        $qualified = isset($kargs['qualified']) ? $kargs['qualified'] : null;
        unset($kargs['qualified']);

        // Remove special words from kargs, convert placeholders
        foreach (array('anchor', 'host', 'protocol') as $key) {
            if (array_key_exists($key, $kargs)) {
                unset($kargs[$key]);
            }
            if (array_key_exists($key . '_', $kargs)) {
                $kargs[$key] = $kargs[$key . '_'];
                unset($kargs[$key . '_']);
            }
        }

        $route = null;
        $routeArgs = array();
        $static = false;
        $encoding = $this->mapper->encoding;
        $environ = $this->mapper->environ;
        $url = '';

        if (isset($routeName)) {
            if (isset($kargs['format']) && isset($this->mapper->routeNames['formatted_' . $routeName])) {
                $route = $this->mapper->routeNames['formatted_' . $routeName];
            } elseif (isset($this->mapper->routeNames[$routeName])) {
                $route = $this->mapper->routeNames[$routeName];
            }

            if ($route && array_key_exists('_static', $route->defaults)) {
                $static = true;
                $url = $route->routePath;
            }

            // No named route found, assume the argument is a relative path
            if ($route === null) {
                $static = true;
                $url = $routeName;
            }

            if ((substr($url, 0, 1) == '/') &&
                isset($environ['SCRIPT_NAME'])) {
                $url = $environ['SCRIPT_NAME'] . $url;
            }

            if ($static) {
                if (!empty($kargs)) {
                    $url .= '?';
                    $query_args = array();
                    foreach ($kargs as $key => $val) {
                        $query_args[] = urlencode(utf8_decode($key)) . '=' .
                            urlencode(utf8_decode($val));
                    }
                    $url .= implode('&', $query_args);
                }
            }
        }

        if (! $static) {
            if ($route) {
                $routeArgs = array($route);
                $newargs = $route->defaults;
                foreach ($kargs as $key => $value) {
                    $newargs[$key] = $value;
                }

                // If this route has a filter, apply it
                if (!empty($route->filter)) {
                    $newargs = call_user_func($route->filter, $newargs);
                }

                $newargs = $this->_subdomainCheck($newargs);
            } else {
                $newargs = $this->_screenArgs($kargs);
            }

            $anchor = (isset($newargs['_anchor'])) ? $newargs['_anchor'] : $anchor;
            unset($newargs['_anchor']);

            $host = (isset($newargs['_host'])) ? $newargs['_host'] : $host;
            unset($newargs['_host']);

            $protocol = (isset($newargs['_protocol'])) ? $newargs['_protocol'] : $protocol;
            unset($newargs['_protocol']);

            $url = $this->mapper->generate($routeArgs, $newargs);
        }

        if (!empty($anchor)) {
            $url .= '#' . self::urlQuote($anchor, $encoding);
        }

        if (!empty($host) || !empty($qualified) || !empty($protocol)) {
            $http_host   = isset($environ['HTTP_HOST']) ? $environ['HTTP_HOST'] : null;
            $server_name = isset($environ['SERVER_NAME']) ? $environ['SERVER_NAME'] : null;
            $fullhost = !is_null($http_host) ? $http_host : $server_name;

            if (empty($host) && empty($qualified)) {
                $host = explode(':', $fullhost);
                $host = $host[0];
            } else if (empty($host)) {
                $host = $fullhost;
            }
            if (empty($protocol)) {
                if (!empty($environ['HTTPS']) && $environ['HTTPS'] != 'off') {
                    $protocol = 'https';
                } else {
                    $protocol = 'http';
                }
            }
            if ($url !== null) {
                $url = $protocol . '://' . $host . $url;
            }
        }

        return $url;
    }

    /**
     * Issues a redirect based on the arguments.
     *
     * Redirects *should* occur as a "302 Moved" header, however the web
     * framework may utilize a different method.
     *
     * All arguments are passed to urlFor() to retrieve the appropriate URL, then
     * the resulting URL it sent to the redirect function as the URL.
     *
     * @param   mixed  $first   First argument in varargs, same as urlFor()
     * @param   mixed  $second  Second argument in varargs
     * @return  mixed           Result of redirect callback
     */
    public function redirectTo($first = array(), $second = array())
    {
        $target = $this->urlFor($first, $second);
        return call_user_func($this->redirect, $target);
    }

    /**
     * Pretty-print a listing of the routes connected to the mapper.
     *
     * @param  stream|null  $stream  Output stream for printing (optional)
     * @param  string|null  $eol     Line ending (optional)
     * @return void
     */
    public function printRoutes($stream = null, $eol = PHP_EOL)
    {
        $printer = new Horde_Routes_Printer($this->mapper);
        $printer->printRoutes($stream, $eol);
    }

    /**
     * Scan a directory for PHP files and use them as controllers.  Used
     * as the default scanner callback for Horde_Routes_Mapper.  See the
     * constructor of that class for more information.
     *
     * Given a directory with:
     *   foo.php, bar.php, baz.php
     * Returns an array:
     *   foo, bar, baz
     *
     * @param  string  $dirname  Directory to scan for controller files
     * @param  string  $prefix   Prefix controller names (optional)
     * @return array             Array of controller names
     */
    public static function controllerScan($dirname = null, $prefix = '')
    {
        $controllers = array();

        if ($dirname === null) {
            return $controllers;
        }

        $baseregexp = preg_quote($dirname . DIRECTORY_SEPARATOR, '/');

        foreach (new RecursiveIteratorIterator(
                 new RecursiveDirectoryIterator($dirname)) as $entry) {
            if (!$entry->isFile()) {
                continue;
            }
            // Match .php files that don't start with an underscore
            if (preg_match('/^[^_]{1,1}.*\.php$/', basename($entry->getFilename())) == 0) {
                continue;
            }
            // Strip off base path: dirname/admin/users.php -> admin/users.php
            $controller = preg_replace("/^$baseregexp(.*)\.php/", '\\1', $entry->getPathname());

            // PrepareController -> prepare_controller -> prepare
            $controller = Horde_String::lower(
                preg_replace('/([a-z])([A-Z])/',
                             "\${1}_\${2}", $controller));
            if (preg_match('/_controller$/', $controller)) {
                $controller = substr($controller, 0, -(strlen('_controller')));
            }

            // Normalize directory separators.
            $controller = str_replace(DIRECTORY_SEPARATOR, '/', $controller);

            // Add to controller list.
            $controllers[] = $prefix . $controller;
        }

        usort($controllers, array('Horde_Routes_Utils', 'longestFirst'));

        return $controllers;
    }

    /**
     * Private function that takes a dict, and screens it against the current
     * request dict to determine what the dict should look like that is used.
     * This is responsible for the requests "memory" of the current.
     */
    private function _screenArgs($kargs)
    {
        if ($this->mapper->explicit && $this->mapper->subDomains) {
            return $this->_subdomainCheck($kargs);
        } else if ($this->mapper->explicit) {
            return $kargs;
        }

        $controllerName = (isset($kargs['controller'])) ? $kargs['controller'] : null;

        if (!empty($controllerName) && substr($controllerName, 0, 1) == '/') {
            // If the controller name starts with '/', ignore route memory
            $kargs['controller'] = substr($kargs['controller'], 1);
            return $kargs;
        } else if (!empty($controllerName) && !array_key_exists('action', $kargs)) {
            // Fill in an action if we don't have one, but have a controller
            $kargs['action'] = 'index';
        }

        $memoryKargs = $this->mapperDict;

        // Remove keys from memory and kargs if kargs has them as null
        foreach ($kargs as $key => $value) {
             if ($value === null) {
                 unset($kargs[$key]);
                 if (array_key_exists($key, $memoryKargs)) {
                     unset($memoryKargs[$key]);
                 }
             }
        }

        // Merge the new args on top of the memory args
        foreach ($kargs as $key => $value) {
            $memoryKargs[$key] = $value;
        }

        // Setup a sub-domain if applicable
        if (!empty($this->mapper->subDomains)) {
            $memoryKargs = $this->_subdomainCheck($memoryKargs);
        }

        return $memoryKargs;
    }

    /**
     * Screen the kargs for a subdomain and alter it appropriately depending
     * on the current subdomain or lack therof.
     */
    private function _subdomainCheck($kargs)
    {
        if ($this->mapper->subDomains) {
            $subdomain = (isset($kargs['subDomain'])) ? $kargs['subDomain'] : null;
            unset($kargs['subDomain']);

            $environ = $this->mapper->environ;
            $http_host   = isset($environ['HTTP_HOST']) ? $environ['HTTP_HOST'] : null;
            $server_name = isset($environ['SERVER_NAME']) ? $environ['SERVER_NAME'] : null;
            $fullhost = !is_null($http_host) ? $http_host : $server_name;

            $hostmatch = explode(':', $fullhost);
            $host = $hostmatch[0];
            $port = '';
            if (count($hostmatch) > 1) {
                $port .= ':' . $hostmatch[1];
            }

            $subMatch = '^.+?\.(' . $this->mapper->domainMatch . ')$';
            $domain = preg_replace("@$subMatch@", '$1', $host);

            if ($subdomain && (substr($host, 0, strlen($subdomain)) != $subdomain)
                    && (! in_array($subdomain, $this->mapper->subDomainsIgnore))) {
                $kargs['_host'] = $subdomain . '.' . $domain . $port;
            } else if (($subdomain === null || in_array($subdomain, $this->mapper->subDomainsIgnore))
                    && $domain != $host) {
                $kargs['_host'] = $domain . $port;
            }
            return $kargs;
        } else {
            return $kargs;
        }
    }

    /**
     * Quote a string containing a URL in a given encoding.
     *
     * @todo This is a placeholder.  Multiple encodings aren't yet supported.
     *
     * @param  string  $url       URL to encode
     * @param  string  $encoding  Encoding to use
     */
    public static function urlQuote($url, $encoding = null)
    {
        if ($encoding === null) {
            return str_replace('%2F', '/', urlencode($url));
        } else {
            return str_replace('%2F', '/', urlencode(utf8_decode($url)));
        }
    }

    /**
     * Callback used by usort() in controllerScan() to sort controller
     * names by the longest first.
     *
     * @param   string  $fst  First string to compare
     * @param   string  $lst  Last string to compare
     * @return  integer       Difference of string length (first - last)
     */
    public static function longestFirst($fst, $lst)
    {
        return strlen($lst) - strlen($fst);
    }

    /**
     */
    public static function arraySubtract($a1, $a2)
    {
        foreach ($a2 as $key) {
            if (in_array($key, $a1)) {
                unset($a1[array_search($key, $a1)]);
            }
        }
        return $a1;
    }
}