OSDN Git Service

FIX: リファレンスにまつわるコードを修正
[nucleus-jp/nucleus-next.git] / nucleus / libs / MANAGER.php
1 <?php
2 /*
3  * Nucleus: PHP/MySQL Weblog CMS (http://nucleuscms.org/)
4  * Copyright (C) 2002-2009 The Nucleus Group
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  * (see nucleus/documentation/index.html#license for more info)
11  */
12 /**
13  * This class makes sure each item/weblog/comment object gets requested from
14  * the database only once, by keeping them in a cache. The class also acts as
15  * a dynamic classloader, loading classes _only_ when they are first needed,
16  * hoping to diminish execution time
17  *
18  * The class is a singleton, meaning that there will be only one object of it
19  * active at all times. The object can be requested using Manager::instance()
20  *
21  * @license http://nucleuscms.org/license.txt GNU General Public License
22  * @copyright Copyright (C) 2002-2009 The Nucleus Group
23  * @version $Id: MANAGER.php 1878 2012-06-17 07:42:07Z sakamocchi $
24  */
25 class Manager
26 {
27         /**
28          * Cached ITEM, BLOG, PLUGIN, KARMA and MEMBER objects. When these objects are requested
29          * through the global $manager object (getItem, getBlog, ...), only the first call
30          * will create an object. Subsequent calls will return the same object.
31          *
32          * The $items, $blogs, ... arrays map an id to an object (for plugins, the name is used
33          * rather than an ID)
34          */
35         private $items;
36         private $blogs;
37         private $plugins;
38         private $karma;
39         private $templates;
40         private $members;
41         private $skins;
42         
43         /**
44          * cachedInfo to avoid repeated SQL queries (see pidInstalled/pluginInstalled/getPidFromName)
45          * e.g. which plugins exists?
46          *
47          * $cachedInfo['installedPlugins'] = array($pid -> $name)
48          */
49         private $cachedInfo;
50         
51         /**
52          * The plugin subscriptionlist
53          *
54          * The subcription array has the following structure
55          *              $subscriptions[$EventName] = array containing names of plugin classes to be
56          *                                                                       notified when that event happens
57          * 
58          * NOTE: this is referred by Comments::addComment() for spamcheck API
59          * TODO: we should add new methods to get this
60          */
61         public $subscriptions;
62         
63         /**
64          * Ticket functions. These are uses by the admin area to make it impossible to simulate certain GET/POST
65          * requests. tickets are user specific
66          */
67         private $currentRequestTicket = '';
68         
69         /**
70          * Returns the only instance of this class. Creates the instance if it
71          * does not yet exists. Users should use this function as
72          * $manager =& Manager::instance(); to get a reference to the object
73          * instead of a copy
74          */
75         static public function &instance()
76         {
77                 static $instance = array();
78                 if ( empty($instance) )
79                 {
80                         $instance[0] = new Manager();
81                 }
82                 return $instance[0];
83         }
84         
85         /**
86          * The constructor of this class initializes the object caches
87          */
88         public function __construct()
89         {
90                 $this->items = array();
91                 $this->blogs = array();
92                 $this->plugins = array();
93                 $this->karma = array();
94                 $this->templates = array();
95                 $this->skins = array();
96                 $this->parserPrefs = array();
97                 $this->cachedInfo = array();
98                 $this->members = array();
99                 return;
100         }
101         
102         /**
103          * Returns the requested item object. If it is not in the cache, it will
104          * first be loaded and then placed in the cache.
105          * Intended use: $item =& $manager->getItem(1234, 0, 0)
106          */
107         public function &getItem($itemid, $allowdraft, $allowfuture)
108         {
109                 /* confirm to cached */
110                 if ( !array_key_exists($itemid, $this->items) )
111                 {
112                         $this->loadClass('ITEM');
113                         $item = Item::getitem($itemid, $allowdraft, $allowfuture);
114                         $this->items[$itemid] = $item;
115                 }
116                 
117                 $item =& $this->items[$itemid];
118                 if ( !$allowdraft && ($item['draft']) )
119                 {
120                         return 0;
121                 }
122                 
123                 $blog =& $this->getBlog($item['blogid']);
124                 if ( !$allowfuture && ($item['timestamp'] > $blog->getCorrectTime()) )
125                 {
126                         return 0;
127                 }
128                 
129                 return $item;
130         }
131         
132         /**
133          * Loads a class if it has not yet been loaded
134          */
135         public function loadClass($name)
136         {
137                 $this->_loadClass($name, $name . '.php');
138                 return;
139         }
140         
141         /**
142          * Checks if an item exists
143          */
144         public function existsItem($id,$future,$draft)
145         {
146                 $this->_loadClass('ITEM','ITEM.php');
147                 return Item::exists($id,$future,$draft);
148         }
149         
150         /**
151          * Checks if a category exists
152          */
153         public function existsCategory($id)
154         {
155                 return (DB::getValue('SELECT COUNT(*) as result FROM '.sql_table('category').' WHERE catid='.intval($id)) > 0);
156         }
157         
158         /**
159          * Returns the blog object for a given blogid
160          */
161         public function &getBlog($blogid)
162         {
163                 if ( !array_key_exists($blogid, $this->blogs) )
164                 {
165                         $this->_loadClass('BLOG','BLOG.php');
166                         $this->blogs[$blogid] = new Blog($blogid);
167                 }
168                 return $this->blogs[$blogid];
169         }
170         
171         /**
172          * Checks if a blog exists
173          */
174         public function existsBlog($name)
175         {
176                 $this->_loadClass('BLOG','BLOG.php');
177                 return Blog::exists($name);
178         }
179         
180         /**
181          * Checks if a blog id exists
182          */
183         public function existsBlogID($id)
184         {
185                 $this->_loadClass('BLOG','BLOG.php');
186                 return Blog::existsID($id);
187         }
188         
189         /**
190          * Returns a previously read template
191          */
192         public function &getTemplate($templateName)
193         {
194                 if ( !array_key_exists($templateName, $this->templates) )
195                 {
196                         $this->_loadClass('Template','TEMPLATE.php');
197                         $tmplate_tmp = Template::read($templateName);
198                         $this->templates[$templateName] =& $tmplate_tmp;
199                 }
200                 return $this->templates[$templateName];
201         }
202         
203         /**
204          * Returns a KARMA object (karma votes)
205          */
206         public function &getKarma($itemid)
207         {
208                 if ( !array_key_exists($itemid, $this->karma) )
209                 {
210                         $this->_loadClass('Karma','KARMA.php');
211                         $this->karma[$itemid] = new Karma($itemid);
212                 }
213                 return $this->karma[$itemid];
214         }
215         
216         /**
217          * Returns a MEMBER object
218          */
219         public function &getMember($memberid)
220         {
221                 if ( !array_key_exists($memberid, $this->members) )
222                 {
223                         $this->_loadClass('Member','MEMBER.php');
224                         $this->members[$memberid] =& Member::createFromID($memberid);;
225                 }
226                 return $this->members[$memberid];
227         }
228         
229         /**
230          * Manager::getSkin()
231          * 
232          * @param       integer $skinid                         ID for skin
233          * @param       string  $action_class           action class for handling skin variables
234          * @param       string  $event_identifier       identifier for event name
235          * @return      object  instance of Skin class
236          */
237         public function &getSkin($skinid, $action_class='Actions', $event_identifier='Skin')
238         {
239                 if ( !array_key_exists($skinid, $this->skins) )
240                 {
241                         $this->_loadClass('Skin', 'SKIN.php');
242                         $this->skins[$skinid] = new Skin($skinid, $action_class, $event_identifier);
243                 }
244                 
245                 return $this->skins[$skinid];
246         }
247         
248         /**
249          * Set the global parser preferences
250          */
251         public function setParserProperty($name, $value)
252         {
253                 $this->parserPrefs[$name] = $value;
254                 return;
255         }
256         
257         /**
258          * Get the global parser preferences
259          */
260         public function getParserProperty($name)
261         {
262                 return $this->parserPrefs[$name];
263         }
264         
265         /**
266          * A helper function to load a class
267          * 
268          *      private
269          */
270         private function _loadClass($name, $filename)
271         {
272                 global $DIR_LIBS;
273                 
274                 if ( !class_exists($name) )
275                 {
276                         include($DIR_LIBS . $filename);
277                 }
278                 return;
279         }
280         
281         /**
282          * Manager::_loadPlugin()
283          * loading a certain plugin
284          * 
285          * @param       string $name plugin name
286          * @return      void
287          */
288         private function _loadPlugin($name)
289         {
290                 global $DIR_PLUGINS, $MYSQL_HANDLER, $MYSQL_PREFIX;
291                 
292                 if ( class_exists($name) )
293                 {
294                         return;
295                 }
296                 
297                 $fileName = "{$DIR_PLUGINS}{$name}.php";
298                 
299                 if ( !file_exists($fileName) )
300                 {
301                         if ( !defined('_MANAGER_PLUGINFILE_NOTFOUND') )
302                         {
303                                 define('_MANAGER_PLUGINFILE_NOTFOUND', 'Plugin %s was not loaded (File not found)');
304                         }
305                         ActionLog::add(WARNING, sprintf(_MANAGER_PLUGINFILE_NOTFOUND, $name)); 
306                         return 0;
307                 }
308                 
309                 // load plugin
310                 include($fileName);
311                 
312                 // check if class exists (avoid errors in eval'd code)
313                 if ( !class_exists($name) )
314                 {
315                         ActionLog::add(WARNING, sprintf(_MANAGER_PLUGINFILE_NOCLASS, $name));
316                         return 0;
317                 }
318                 
319                 // add to plugin array
320                 $this->plugins[$name] = new $name();
321                 
322                 // get plugid
323                 $this->plugins[$name]->setID($this->getPidFromName($name));
324                 
325                 // unload plugin if a prefix is used and the plugin cannot handle this
326                 if ( ($MYSQL_PREFIX != '')
327                   && !$this->plugins[$name]->supportsFeature('SqlTablePrefix') )
328                 {
329                         unset($this->plugins[$name]);
330                         ActionLog::add(WARNING, sprintf(_MANAGER_PLUGINTABLEPREFIX_NOTSUPPORT, $name));
331                         return 0;
332                 }
333                 
334                 // unload plugin if using non-mysql handler and plugin does not support it 
335                 if ( (!in_array('mysql',$MYSQL_HANDLER))
336                   && !$this->plugins[$name]->supportsFeature('SqlApi') )
337                 {
338                         unset($this->plugins[$name]);
339                         ActionLog::add(WARNING, sprintf(_MANAGER_PLUGINSQLAPI_NOTSUPPORT, $name));
340                         return 0;
341                 }
342                 
343                 // call init method
344                 $this->plugins[$name]->init();
345                 
346                 return;
347         }
348
349         /**
350          * Manager:getPlugin()
351          * Returns a PLUGIN object
352          * 
353          * @param       string  $name   name of plugin
354          * @return      object  plugin object
355          */
356         public function &getPlugin($name)
357         {
358                 // retrieve the name of the plugin in the right capitalisation
359                 $name = $this->getUpperCaseName ($name);
360                 
361                 // get the plugin       
362                 $plugin =& $this->plugins[$name]; 
363                 
364                 if ( !$plugin )
365                 {
366                         // load class if needed
367                         $this->_loadPlugin($name);
368                         $plugin =& $this->plugins[$name];
369                 }
370                 return $plugin;
371         }
372         
373         /**
374          * Manager::pluginLoaded()
375          * Checks if the given plugin IS loaded or not
376          * 
377          * @param       string  $name   name of plugin
378          * @return      object  plugin object
379          */
380         public function &pluginLoaded($name)
381         {
382                 $plugin =& $this->plugins[$name];
383                 return $plugin;
384         }
385         
386         /**
387          * Manager::pidLoaded()
388          * 
389          * @param       integer $pid    id for plugin
390          * @return      object  plugin object
391          */
392         public function &pidLoaded($pid)
393         {
394                 $plugin=false;
395                 reset($this->plugins);
396                 while ( list($name) = each($this->plugins) )
397                 {
398                         if ( $pid!=$this->plugins[$name]->getId() )
399                         {
400                                 continue;
401                         }
402                         $plugin= & $this->plugins[$name];
403                         break;
404                 }
405                 return $plugin;
406         }
407         
408         /**
409          * Manager::pluginInstalled()
410          * checks if the given plugin IS installed or not
411          * 
412          * @param       string  $name   name of plugin
413          * @return      boolean exists or not
414          */
415         public function pluginInstalled($name)
416         {
417                 $this->_initCacheInfo('installedPlugins');
418                 return ($this->getPidFromName($name) != -1);
419         }
420
421         /**
422          * Manager::pidInstalled()
423          * checks if the given plugin IS installed or not
424          * 
425          * @param       integer $pid    id of plugin
426          * @return      boolean exists or not
427          */
428         public function pidInstalled($pid)
429         {
430                 $this->_initCacheInfo('installedPlugins');
431                 return ($this->cachedInfo['installedPlugins'][$pid] != '');
432         }
433         
434         /**
435          * Manager::getPidFromName()
436          * 
437          * @param       string  $name   name of plugin
438          * @return      mixed   id for plugin or -1 if not exists
439          */
440         public function getPidFromName($name)
441         {
442                 $this->_initCacheInfo('installedPlugins');
443                 foreach ( $this->cachedInfo['installedPlugins'] as $pid => $pfile )
444                 {
445                         if (strtolower($pfile) == strtolower($name))
446                         {
447                                 return $pid;
448                         }
449                 }
450                 return -1;
451         }
452         
453         /**
454          * Manager::getPluginNameFromPid()
455          * 
456          * @param       string  $pid    ID for plugin
457          * @return      string  name of plugin
458          */
459         public function getPluginNameFromPid($pid)
460         {
461                 if ( !array_key_exists($pid, $this->cachedInfo['installedPlugins']) )
462                 {
463                         $query = 'SELECT pfile FROM %s WHERE pid=%d;';
464                         $query = sprintf($query, sql_table('plugin'), (integer) $pid);
465                         return DB::getValue($query);
466                 }
467                 return $this->cachedInfo['installedPlugins'][$pid];
468         }
469         
470         /**
471          * Manager::getUpperCaseName()
472          * Retrieve the name of a plugin in the right capitalisation
473          * 
474          * @param       string  $name   name of plugin
475          * @return      string  name according to UpperCamelCase
476          */
477         public function getUpperCaseName ($name)
478         {
479                 $this->_initCacheInfo('installedPlugins');
480                 foreach ( $this->cachedInfo['installedPlugins'] as $pid => $pfile )
481                 {
482                         if ( strtolower($pfile) == strtolower($name) )
483                         {
484                                 return $pfile;
485                         }
486                 }
487                 return -1;
488         }
489         
490         /**
491          * Manager::clearCachedInfo()
492          * 
493          * @param       string  $what
494          * @return      void
495          */
496         public function clearCachedInfo($what)
497         {
498                 unset($this->cachedInfo[$what]);
499                 return;
500         }
501         
502         /**
503          * Manager::_initCacheInfo()
504          * Loads some info on the first call only
505          * 
506          * @param       string  $what   'installedPlugins'
507          * @return      void
508          */
509         private function _initCacheInfo($what)
510         {
511                 if ( array_key_exists($what, $this->cachedInfo)
512                   && is_array($this->cachedInfo[$what]) )
513                 {
514                         return;
515                 }
516                 
517                 switch ($what)
518                 {
519                         // 'installedPlugins' = array ($pid => $name)
520                         case 'installedPlugins':
521                                 $this->cachedInfo['installedPlugins'] = array();
522                                 $res = DB::getResult('SELECT pid, pfile FROM ' . sql_table('plugin'));
523                                 foreach ( $res as $row )
524                                 {
525                                         $this->cachedInfo['installedPlugins'][$row['pid']] = $row['pfile'];
526                                 }
527                                 break;
528                 }
529                 return;
530         }
531         
532         /**
533          * Manager::notify()
534          * A function to notify plugins that something has happened. Only the plugins
535          * that are subscribed to the event will get notified.
536          * Upon the first call, the list of subscriptions will be fetched from the
537          * database. The plugins itsself will only get loaded when they are first needed
538          *
539          * @param       string  $eventName      Name of the event (method to be called on plugins)
540          * @param       string  $data           Can contain any type of data,
541          *                                                              depending on the event type. Usually this is an itemid, blogid, ...
542          *                                                              but it can also be an array containing multiple values
543          * @return      void
544          */
545         public function notify($eventName, &$data)
546         {
547                 // load subscription list if needed
548                 if ( !is_array($this->subscriptions) )
549                 {
550                         $this->_loadSubscriptions();
551                 }
552                 
553                 // get listening objects
554                 $listeners = false;
555                 if ( array_key_exists($eventName, $this->subscriptions)
556                   && !empty($this->subscriptions[$eventName]) )
557                 {
558                         $listeners = $this->subscriptions[$eventName];
559                 }
560                 
561                 // notify all of them
562                 if ( is_array($listeners) )
563                 {
564                         foreach( $listeners as $listener )
565                         {
566                                 // load class if needed
567                                 $this->_loadPlugin($listener);
568                                 
569                                 // do notify (if method exists)
570                                 if ( array_key_exists($listener, $this->plugins)
571                                   && !empty($this->plugins[$listener])
572                                   && method_exists($this->plugins[$listener], 'event_' . $eventName) )
573                                 {
574                                         call_user_func(array($this->plugins[$listener], 'event_' . $eventName), $data);
575                                 }
576                         }
577                 }
578                 return;
579         }
580         
581         /**
582          * Manager::_loadSubscriptions()
583          * Loads plugin subscriptions
584          * 
585          * @param       void
586          * @return      void
587          */
588         private function _loadSubscriptions()
589         {
590                 // initialize as array
591                 $this->subscriptions = array();
592                 
593                 $query = "SELECT p.pfile as pfile, e.event as event"
594                        . " FROM %s as e, %s as p"
595                        . " WHERE e.pid=p.pid ORDER BY p.porder ASC";
596                 $query = sprintf($query, sql_table('plugin_event'), sql_table('plugin'));
597                 $res = DB::getResult($query);
598                 
599                 foreach ( $res as $row )
600                 {
601                         $pluginName = $row['pfile'];
602                         $eventName = $row['event'];
603                         $this->subscriptions[$eventName][] = $pluginName;
604                 }
605                 return;
606         }
607         
608         /**
609          * Manager::getNumberOfSubscribers()
610          * 
611          * @param       string  $event  name of events
612          * @return      integer number of event subscriber
613          */
614         public function getNumberOfSubscribers($event)
615         {
616                 $query = 'SELECT COUNT(*) as count FROM %s WHERE event=%s;';
617                 $query = sprintf($query, sql_table('plugin_event'), DB::quoteValue($event));
618                 return (integer) DB::getValue($query);
619         }
620         
621         /**
622          * Manager::addTicketToUrl()
623          * GET requests: Adds ticket to URL (URL should NOT be html-encoded!, ticket is added at the end)
624          * 
625          * @param       string  url     string for URI
626          * @return      void
627          */
628         public function addTicketToUrl($url)
629         {
630                 $ticketCode = 'ticket=' . $this->_generateTicket();
631                 if ( i18n::strpos($url, '?') === FALSE )
632                 {
633                         $ticketCode = "{$url}?{$ticketCode}";
634                 }
635                 else
636                 {
637                         $ticketCode = "{$url}&{$ticketCode}";
638                 }
639                 return $ticketCode;
640         }
641         
642         /**
643          * Manager::addTicketHidden()
644          * POST requests: Adds ticket as hidden formvar
645          * 
646          * @param       void
647          * @return      void
648          */
649         public function addTicketHidden()
650         {
651                 $ticket = $this->_generateTicket();
652                 echo '<input type="hidden" name="ticket" value="', Entity::hsc($ticket), '" />';
653                 return;
654         }
655         
656         /**
657          * Manager::getNewTicket()
658          * Get a new ticket
659          * (xmlHTTPRequest AutoSaveDraft uses this to refresh the ticket)
660          * 
661          * @param       void
662          * @return      string  string of ticket
663          */
664         public function getNewTicket()
665         {
666                 $this->currentRequestTicket = '';
667                 return $this->_generateTicket();
668         }
669         
670         /**
671          * Manager::checkTicket()
672          * Checks the ticket that was passed along with the current request
673          * 
674          * @param       void
675          * @return      boolean correct or not
676          */
677         public function checkTicket()
678         {
679                 global $member;
680                 
681                 // get ticket from request
682                 $ticket = requestVar('ticket');
683                 
684                 // no ticket -> don't allow
685                 if ( $ticket == '' )
686                 {
687                         return FALSE;
688                 }
689                 
690                 // remove expired tickets first
691                 $this->_cleanUpExpiredTickets();
692                 
693                 // get member id
694                 if (!$member->isLoggedIn())
695                 {
696                         $memberId = -1;
697                 }
698                 else
699                 {
700                         $memberId = $member->getID();
701                 }
702                 
703                 // check if ticket is a valid one
704                 $query = sprintf('SELECT COUNT(*) as result FROM %s WHERE member=%d and ticket=%s',
705                         sql_table('tickets'),
706                         intval($memberId),
707                         DB::quoteValue($ticket)
708                 );
709                 
710                 /*
711                  * NOTE:
712                  * [in the original implementation, the checked ticket was deleted. This would lead to invalid
713                  * tickets when using the browsers back button and clicking another link/form
714                  * leaving the keys in the database is not a real problem, since they're member-specific and
715                  * only valid for a period of one hour]
716                  */
717                 if ( DB::getValue($query) != 1 )
718                 {
719                         return FALSE;
720                 }
721                 
722                 return TRUE;
723         }
724
725         /**
726          * Manager::_cleanUpExpiredTickets()
727          * Removes the expired tickets
728          * 
729          * @param       void
730          * @return      void
731          */
732         private function _cleanUpExpiredTickets()
733         {
734                 // remove tickets older than 1 hour
735                 $oldTime = time() - 60 * 60;
736                 $query = 'DELETE FROM %s WHERE ctime < %s';
737                 $query = sprintf($query, sql_table('tickets'), DB::formatDateTime($oldTime));
738                 DB::execute($query);
739                 return;
740         }
741         
742         /**
743          * Manager::_generateTicket()
744          * Generates/returns a ticket (one ticket per page request)
745          * 
746          * @param       void
747          * @return      void
748          */
749         private function _generateTicket()
750         {
751                 if ( $this->currentRequestTicket == '' )
752                 {
753                         // generate new ticket (only one ticket will be generated per page request)
754                         // and store in database
755                         global $member;
756                         // get member id
757                         if ( !$member->isLoggedIn() )
758                         {
759                                 $memberId = -1;
760                         }
761                         else
762                         {
763                                 $memberId = $member->getID();
764                         }
765                         
766                         $ok = false;
767                         while ( !$ok )
768                         {
769                                 // generate a random token
770                                 srand((double)microtime()*1000000);
771                                 $ticket = md5(uniqid(rand(), true));
772                                 
773                                 // add in database as non-active
774                                 $query = 'INSERT INTO %s (ticket, member, ctime) VALUES (%s, %d, %s)';
775                                 $query = sprintf($query, sql_table('tickets'), DB::quoteValue($ticket), (integer) $memberId, DB::formatDateTime());
776                                 
777                                 if ( DB::execute($query) !== FALSE )
778                                 {
779                                         $ok = true;
780                                 }
781                         }
782                         $this->currentRequestTicket = $ticket;
783                 }
784                 return $this->currentRequestTicket;
785         }
786 }
787