Completed
Push — master ( 791f1f...546eff )
by Thierry
01:43
created

CallableObject::generateHash()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * CallableObject.php - Jaxon callable object plugin
5
 *
6
 * This class registers user defined callable objects, generates client side javascript code,
7
 * and calls their methods on user request
8
 *
9
 * @package jaxon-core
10
 * @author Jared White
11
 * @author J. Max Wilson
12
 * @author Joseph Woolley
13
 * @author Steffen Konerow
14
 * @author Thierry Feuzeu <[email protected]>
15
 * @copyright Copyright (c) 2005-2007 by Jared White & J. Max Wilson
16
 * @copyright Copyright (c) 2008-2010 by Joseph Woolley, Steffen Konerow, Jared White  & J. Max Wilson
17
 * @copyright 2016 Thierry Feuzeu <[email protected]>
18
 * @license https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
19
 * @link https://github.com/jaxon-php/jaxon-core
20
 */
21
22
namespace Jaxon\Request\Plugin;
23
24
use Jaxon\Jaxon;
25
use Jaxon\Plugin\Request as RequestPlugin;
26
27
class CallableObject extends RequestPlugin
28
{
29
    use \Jaxon\Utils\Traits\Config;
30
    use \Jaxon\Utils\Traits\Manager;
31
    use \Jaxon\Utils\Traits\Validator;
32
    use \Jaxon\Utils\Traits\Translator;
33
34
    /**
35
     * The registered callable objects
36
     *
37
     * @var array
38
     */
39
    protected $aCallableObjects;
40
41
    /**
42
     * The classpaths of the registered callable objects
43
     *
44
     * @var array
45
     */
46
    protected $aClassPaths;
47
48
    /**
49
     * Directories where Jaxon classes to be registered are found
50
     *
51
     * @var array
52
     */
53
    private $aClassDirs = [];
54
55
    /**
56
     * True if the Composer autoload is enabled
57
     *
58
     * @var boolean
59
     */
60
    private $bAutoloadEnabled;
61
62
    /**
63
     * The Composer autoloader
64
     *
65
     * @var Autoloader
66
     */
67
    private $xAutoloader;
68
69
    /**
70
     * The value of the class parameter of the incoming Jaxon request
71
     *
72
     * @var string
73
     */
74
    protected $sRequestedClass;
75
76
    /**
77
     * The value of the method parameter of the incoming Jaxon request
78
     *
79
     * @var string
80
     */
81
    protected $sRequestedMethod;
82
83
    public function __construct()
84
    {
85
        $this->aCallableObjects = [];
86
        $this->aClassPaths = [];
87
88
        $this->bAutoloadEnabled = true;
89
        $this->xAutoloader = null;
90
91
        $this->sRequestedClass = null;
92
        $this->sRequestedMethod = null;
93
94
        if(!empty($_GET['jxncls']))
95
        {
96
            $this->sRequestedClass = $_GET['jxncls'];
97
        }
98
        if(!empty($_GET['jxnmthd']))
99
        {
100
            $this->sRequestedMethod = $_GET['jxnmthd'];
101
        }
102
        if(!empty($_POST['jxncls']))
103
        {
104
            $this->sRequestedClass = $_POST['jxncls'];
105
        }
106
        if(!empty($_POST['jxnmthd']))
107
        {
108
            $this->sRequestedMethod = $_POST['jxnmthd'];
109
        }
110
    }
111
112
    /**
113
     * Return the name of this plugin
114
     *
115
     * @return string
116
     */
117
    public function getName()
118
    {
119
        return Jaxon::CALLABLE_OBJECT;
120
    }
121
122
    /**
123
     * Use the Composer autoloader
124
     *
125
     * @return void
126
     */
127
    public function useComposerAutoloader()
128
    {
129
        $this->bAutoloadEnabled = true;
130
        $this->xAutoloader = require(__DIR__ . '/../../../../autoload.php');
131
    }
132
133
    /**
134
     * Disable the autoloader in the library
135
     *
136
     * The user shall provide an alternative autoload system.
137
     *
138
     * @return void
139
     */
140
    public function disableAutoload()
141
    {
142
        $this->bAutoloadEnabled = false;
143
        $this->xAutoloader = null;
144
    }
145
146
    /**
147
     * Register a user defined callable object
148
     *
149
     * @param array         $aArgs                An array containing the callable object specification
150
     *
151
     * @return array
152
     */
153
    public function register($aArgs)
154
    {
155
        if(count($aArgs) < 2)
156
        {
157
            return false;
158
        }
159
160
        $sType = $aArgs[0];
161
        if($sType != Jaxon::CALLABLE_OBJECT)
162
        {
163
            return false;
164
        }
165
166
        $sCallableObject = $aArgs[1];
167
        if(!is_string($sCallableObject) || !class_exists($sCallableObject))
168
        {
169
            throw new \Jaxon\Exception\Error($this->trans('errors.objects.invalid-declaration'));
170
        }
171
        $sCallableObject = trim($sCallableObject, '\\');
172
        $this->aCallableObjects[] = $sCallableObject;
173
174
        $aOptions = count($aArgs) > 2 ? $aArgs[2] : [];
175
        if(is_string($aOptions))
176
        {
177
            $aOptions = ['namespace' => $aOptions];
178
        }
179
        if(!is_array($aOptions))
180
        {
181
            throw new \Jaxon\Exception\Error($this->trans('errors.objects.invalid-declaration'));
182
        }
183
184
        // Save the classpath and the separator in this class
185
        if(key_exists('*', $aOptions) && is_array($aOptions['*']))
186
        {
187
            $_aOptions = $aOptions['*'];
188
            $sSeparator = '.';
189
            if(key_exists('separator', $_aOptions))
190
            {
191
                $sSeparator = trim($_aOptions['separator']);
192
            }
193
            if(!in_array($sSeparator, ['.', '_']))
194
            {
195
                $sSeparator = '.';
196
            }
197
            $_aOptions['separator'] = $sSeparator;
198
199
            if(array_key_exists('classpath', $_aOptions))
200
            {
201
                $_aOptions['classpath'] = trim($_aOptions['classpath'], ' \\._');
202
                // Save classpath with "\" in the parameters
203
                $_aOptions['classpath'] = str_replace(['.', '_'], ['\\', '\\'], $_aOptions['classpath']);
204
                // Save classpath with separator locally
205
                $this->aClassPaths[] = str_replace('\\', $sSeparator, $_aOptions['classpath']);
206
            }
207
        }
208
209
        jaxon()->di()->set($sUserFunction, function() use($sCallableObject, $aOptions) {
0 ignored issues
show
Bug introduced by
The variable $sUserFunction does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
210
            $xCallableObject = new \Jaxon\Request\Support\CallableObject($sCallableObject);
211
212
            foreach($aOptions as $sMethod => $aValue)
213
            {
214
                foreach($aValue as $sName => $sValue)
215
                {
216
                    $xCallableObject->configure($sMethod, $sName, $sValue);
217
                }
218
            }
219
220
            return $xCallableObject;
221
        });
222
223
        return true;
224
    }
225
226
    /**
227
     * Add a path to the class directories
228
     *
229
     * @param string            $sDirectory             The path to the directory
230
     * @param string|null       $sNamespace             The associated namespace
231
     * @param string            $sSeparator             The character to use as separator in javascript class names
232
     * @param array             $aProtected             The functions that are not to be exported
233
     *
234
     * @return boolean
235
     */
236
    public function addClassDir($sDirectory, $sNamespace = '', $sSeparator = '.', array $aProtected = [])
237
    {
238
        if(!is_dir(($sDirectory = trim($sDirectory))))
239
        {
240
            return false;
241
        }
242
        // Only '.' and '_' are allowed to be used as separator. Any other value is ignored and '.' is used instead.
243
        if(($sSeparator = trim($sSeparator)) != '_')
244
        {
245
            $sSeparator = '.';
246
        }
247
        if(!($sNamespace = trim($sNamespace, ' \\')))
248
        {
249
            $sNamespace = '';
250
        }
251
        if(($sNamespace))
252
        {
253
            // If there is an autoloader, register the dir with PSR4 autoloading
254
            if(($this->xAutoloader))
255
            {
256
                $this->xAutoloader->setPsr4($sNamespace . '\\', $sDirectory);
257
            }
258
        }
259
        elseif(($this->xAutoloader))
260
        {
261
            // If there is an autoloader, register the dir with classmap autoloading
262
            $itDir = new RecursiveDirectoryIterator($sDirectory);
263
            $itFile = new RecursiveIteratorIterator($itDir);
264
            // Iterate on dir content
265
            foreach($itFile as $xFile)
266
            {
267
                // skip everything except PHP files
268
                if(!$xFile->isFile() || $xFile->getExtension() != 'php')
269
                {
270
                    continue;
271
                }
272
                $this->xAutoloader->addClassMap(array($xFile->getBasename('.php') => $xFile->getPathname()));
273
            }
274
        }
275
        $this->aClassDirs[] = array(
276
            'directory' => rtrim($sDirectory, DIRECTORY_SEPARATOR),
277
            'namespace' => $sNamespace,
278
            'separator' => $sSeparator,
279
            'protected' => $aProtected
280
        );
281
        return true;
282
    }
283
284
    /**
285
     * Register an instance of a given class from a file
286
     *
287
     * @param object            $xFile                  The PHP file containing the class
288
     * @param string            $sDirectory             The path to the directory
289
     * @param string|''         $sNamespace             The associated namespace
0 ignored issues
show
Documentation introduced by
The doc-type string|'' could not be parsed: Unknown type name "''" at position 7. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
290
     * @param string            $sSeparator             The character to use as separator in javascript class names
291
     * @param array             $aProtected             The functions that are not to be exported
292
     * @param array             $aOptions               The options to register the class with
293
     *
294
     * @return void
295
     */
296
    protected function registerClassFromFile($xFile, $sDirectory, $sNamespace = '', $sSeparator = '.',
297
        array $aProtected = [], array $aOptions = [])
298
    {
299
        $sDS = DIRECTORY_SEPARATOR;
300
        // Get the corresponding class path and name
301
        $sClassPath = substr($xFile->getPath(), strlen($sDirectory));
302
        $sClassPath = str_replace($sDS, '\\', trim($sClassPath, $sDS));
303
        $sClassName = $xFile->getBasename('.php');
304
        if(($sNamespace))
305
        {
306
            $sClassPath = ($sClassPath) ? $sNamespace . '\\' . $sClassPath : $sNamespace;
307
            $sClassName = '\\' . $sClassPath . '\\' . $sClassName;
308
        }
309
        // Require the file only if autoload is enabled but there is no autoloader
310
        if(($this->bAutoloadEnabled) && !($this->xAutoloader))
311
        {
312
            require_once($xFile->getPathname());
313
        }
314
        // Create and register an instance of the class
315
        if(!array_key_exists('*', $aOptions) || !is_array($aOptions['*']))
316
        {
317
            $aOptions['*'] = [];
318
        }
319
        $aOptions['*']['separator'] = $sSeparator;
320
        if(($sNamespace))
321
        {
322
            $aOptions['*']['namespace'] = $sNamespace;
323
        }
324
        if(($sClassPath))
325
        {
326
            $aOptions['*']['classpath'] = $sClassPath;
327
        }
328
        // Filter excluded methods
329
        $aProtected = array_filter($aProtected, function ($sName) {return is_string($sName);});
330
        if(count($aProtected) > 0)
331
        {
332
            $aOptions['*']['protected'] = $aProtected;
333
        }
334
        $this->register(array(Jaxon::CALLABLE_OBJECT, $sClassName, $aOptions));
335
    }
336
337
    /**
338
     * Register callable objects from all class directories
339
     *
340
     * @param array             $aOptions               The options to register the classes with
341
     *
342
     * @return void
343
     */
344
    public function registerClasses(array $aOptions = [])
345
    {
346
        $sDS = DIRECTORY_SEPARATOR;
347
        // Change the keys in $aOptions to have "\" as separator
348
        $aNewOptions = [];
349
        foreach($aOptions as $key => $aOption)
350
        {
351
            $key = trim(str_replace(['.', '_'], ['\\', '\\'], $key), ' \\');
352
            $aNewOptions[$key] = $aOption;
353
        }
354
355
        foreach($this->aClassDirs as $aClassDir)
356
        {
357
            // Get the directory
358
            $sDirectory = $aClassDir['directory'];
359
            // Get the namespace
360
            $sNamespace = $aClassDir['namespace'];
361
362
            $itDir = new RecursiveDirectoryIterator($sDirectory);
363
            $itFile = new RecursiveIteratorIterator($itDir);
364
            // Iterate on dir content
365
            foreach($itFile as $xFile)
366
            {
367
                // skip everything except PHP files
368
                if(!$xFile->isFile() || $xFile->getExtension() != 'php')
369
                {
370
                    continue;
371
                }
372
373
                // Get the class name
374
                $sClassPath = substr($xFile->getPath(), strlen($sDirectory));
375
                $sClassPath = trim(str_replace($sDS, '\\', $sClassPath), '\\');
376
                $sClassName = $xFile->getBasename('.php');
377
                if(($sClassPath))
378
                {
379
                    $sClassName = $sClassPath . '\\' . $sClassName;
380
                }
381
                if(($sNamespace))
382
                {
383
                    $sClassName = $sNamespace . '\\' . $sClassName;
384
                }
385
                // Get the class options
386
                $aClassOptions = [];
387
                if(array_key_exists($sClassName, $aNewOptions))
388
                {
389
                    $aClassOptions = $aNewOptions[$sClassName];
390
                }
391
392
                $this->registerClassFromFile($xFile, $sDirectory, $sNamespace,
393
                    $aClassDir['separator'], $aClassDir['protected'], $aClassOptions);
394
            }
395
        }
396
    }
397
398
    /**
399
     * Register an instance of a given class
400
     *
401
     * @param string            $sClassName             The name of the class to be registered
402
     * @param array             $aOptions               The options to register the class with
403
     *
404
     * @return bool
405
     */
406
    public function registerClass($sClassName, array $aOptions = [])
407
    {
408
        if(!($sClassName = trim($sClassName, ' \\._')))
409
        {
410
            return false;
411
        }
412
        $sDS = DIRECTORY_SEPARATOR;
413
414
        // Replace "." and "_" with antislashes, and set the class path.
415
        $sClassName = str_replace(['.', '_'], ['\\', '\\'], $sClassName);
416
        $sClassPath = '';
417
        if(($nLastSlashPosition = strrpos($sClassName, '\\')) !== false)
418
        {
419
            $sClassPath = substr($sClassName, 0, $nLastSlashPosition);
420
            $sClassName = substr($sClassName, $nLastSlashPosition + 1);
421
        }
422
        // Path to the file, relative to a registered directory.
423
        $sPartPath = str_replace('\\', $sDS, $sClassPath) . $sDS . $sClassName . '.php';
424
425
        // Search for the class file in all directories.
426
        foreach($this->aClassDirs as $aClassDir)
427
        {
428
            // Get the separator
429
            $sSeparator = $aClassDir['separator'];
430
            // Get the namespace
431
            $sNamespace = $aClassDir['namespace'];
432
            $nLen = strlen($sNamespace);
433
            $sFullPath = '';
434
            // Check if the class belongs to the namespace
435
            if(($sNamespace) && substr($sClassPath, 0, $nLen) == $sNamespace)
436
            {
437
                $sFullPath = $aClassDir['directory'] . $sDS . substr($sPartPath, $nLen + 1);
438
            }
439
            elseif(!($sNamespace))
440
            {
441
                $sFullPath = $aClassDir['directory'] . $sDS . $sPartPath;
442
            }
443
            if(($sFullPath) && is_file($sFullPath))
444
            {
445
                // Found the file in this directory
446
                $xFileInfo = new \SplFileInfo($sFullPath);
447
                $sDirectory = $aClassDir['directory'];
448
                $aProtected = $aClassDir['protected'];
449
                $this->registerClassFromFile($xFileInfo, $sDirectory, $sNamespace, $sSeparator, $aProtected, $aOptions);
450
                return true;
451
            }
452
        }
453
        return false;
454
    }
455
456
    /**
457
     * Generate a hash for the registered callable objects
458
     *
459
     * @return string
460
     */
461
    public function generateHash()
462
    {
463
        $di = jaxon()->di();
464
        $sHash = '';
465
        foreach($this->aCallableObjects as $sName)
466
        {
467
            $xCallableObject = $di->get($sName);
468
            $sHash .= $sName . implode('|', $xCallableObject->getMethods());
469
        }
470
        return md5($sHash);
471
    }
472
473
    /**
474
     * Generate client side javascript code for the registered callable objects
475
     *
476
     * @return string
477
     */
478
    public function getScript()
479
    {
480
        $sJaxonPrefix = $this->getOption('core.prefix.class');
481
        // Generate code for javascript objects declaration
482
        $code = '';
483
        $classes = [];
484
        foreach($this->aClassPaths as $sClassPath)
485
        {
486
            $offset = 0;
487
            $sClassPath .= '.Null'; // This is a sentinel. The last token is not processed in the while loop.
488
            while(($dotPosition = strpos($sClassPath, '.', $offset)) !== false)
489
            {
490
                $class = substr($sClassPath, 0, $dotPosition);
491
                // Generate code for this object
492
                if(!array_key_exists($class, $classes))
493
                {
494
                    $code .= "$sJaxonPrefix$class = {};\n";
495
                    $classes[$class] = $class;
496
                }
497
                $offset = $dotPosition + 1;
498
            }
499
        }
500
        // Generate code for javascript methods
501
        $di = jaxon()->di();
502
        foreach($this->aCallableObjects as $sName)
503
        {
504
            $xCallableObject = $di->get($sName);
505
            $code .= $xCallableObject->getScript();
506
        }
507
        return $code;
508
    }
509
510
    /**
511
     * Check if this plugin can process the incoming Jaxon request
512
     *
513
     * @return boolean
514
     */
515
    public function canProcessRequest()
516
    {
517
        // Check the validity of the class name
518
        if(($this->sRequestedClass) && !$this->validateClass($this->sRequestedClass))
519
        {
520
            $this->sRequestedClass = null;
521
            $this->sRequestedMethod = null;
522
        }
523
        // Check the validity of the method name
524
        if(($this->sRequestedMethod) && !$this->validateMethod($this->sRequestedMethod))
525
        {
526
            $this->sRequestedClass = null;
527
            $this->sRequestedMethod = null;
528
        }
529
        return ($this->sRequestedClass != null && $this->sRequestedMethod != null);
530
    }
531
532
    /**
533
     * Process the incoming Jaxon request
534
     *
535
     * @return boolean
536
     */
537
    public function processRequest()
538
    {
539
        if(!$this->canProcessRequest())
540
        {
541
            return false;
542
        }
543
544
        $aArgs = $this->getRequestManager()->process();
545
546
        // Find the requested method
547
        $xCallableObject = $this->getCallableObject($this->sRequestedClass);
548
        if(!$xCallableObject || !$xCallableObject->hasMethod($this->sRequestedMethod))
549
        {
550
            // Unable to find the requested object or method
551
            throw new \Jaxon\Exception\Error($this->trans('errors.objects.invalid',
552
                ['class' => $this->sRequestedClass, 'method' => $this->sRequestedMethod]));
553
        }
554
555
        // Call the requested method
556
        $xCallableObject->call($this->sRequestedMethod, $aArgs);
557
        return true;
558
    }
559
560
    /**
561
     * Find a callable object by class name
562
     *
563
     * @param string        $sClassName            The class name of the callable object
564
     *
565
     * @return object
566
     */
567
    public function getCallableObject($sClassName)
568
    {
569
        // Replace all separators ('.' and '_') with antislashes, and remove the antislashes
570
        // at the beginning and the end of the class name.
571
        $sClassName = trim(str_replace(['.', '_'], ['\\', '\\'], (string)$sClassName), '\\');
572
        // Register an instance of the requested class, if it isn't yet
573
        if(!key_exists($sClassName, $this->aCallableObjects))
574
        {
575
            $this->getPluginManager()->registerClass($sClassName);
576
        }
577
        return key_exists($sClassName, $this->aCallableObjects) ? jaxon()->di()->get($sClassName) : null;
578
    }
579
580
    /**
581
     * Find a user registered callable object by class name
582
     *
583
     * @param string        $sClassName            The class name of the callable object
584
     *
585
     * @return object
586
     */
587
    public function getRegisteredObject($sClassName)
588
    {
589
        // Get the corresponding callable object
590
        $xCallableObject = $this->getCallableObject($sClassName);
591
        return ($xCallableObject) ? $xCallableObject->getRegisteredObject() : null;
592
    }
593
}
594