Merge remote branch 'origin/core/1.1.1/maint' into core/master
authorFlorent Bruneau <florent.bruneau@polytechnique.org>
Thu, 16 Dec 2010 20:13:55 +0000 (21:13 +0100)
committerFlorent Bruneau <florent.bruneau@polytechnique.org>
Thu, 16 Dec 2010 20:13:55 +0000 (21:13 +0100)
14 files changed:
classes/env.php
classes/platal.php
classes/plcache.php
classes/pldbtableentry.php
classes/pldict.php
classes/plerrorreport.php
classes/plexportable.php [new file with mode: 0644]
classes/plfilter.php
classes/plflagset.php
classes/plpage.php
classes/s.php
include/misc.inc.php
include/platal.inc.php
modules/core.php

index cecee84..7a2144f 100644 (file)
@@ -67,8 +67,8 @@ class Env
 
     public static function i($key, $default = 0)
     {
-        $i = Env::_get($key, $default);
-        return is_numeric($i) ? intval($i) : $default;
+        $i = to_integer(Env::_get($key, $default));
+        return $i === false ? $default : $i;
     }
 
     public static function l(array $keys)
@@ -125,8 +125,8 @@ class Post
 
     public static function i($key, $default = 0)
     {
-        $i = Post::_get($key, $default);
-        return is_numeric($i) ? intval($i) : $default;
+        $i = to_integer(Post::_get($key, $default));
+        return $i === false ? $default : $i;
     }
 
      public static function l(array $keys)
@@ -183,8 +183,8 @@ class Get
 
     public static function i($key, $default = 0)
     {
-        $i = Get::_get($key, $default);
-        return is_numeric($i) ? intval($i) : $default;
+        $i = to_integer(Get::_get($key, $default));
+        return $i === false ? $default : $i;
     }
 
     public static function l(array $keys)
@@ -258,8 +258,8 @@ class Cookie
 
     public static function i($key, $default = 0)
     {
-        $i = Cookie::_get($key, $default);
-        return is_numeric($i) ? intval($i) : $default;
+        $i = to_integer(Cookie::_get($key, $default));
+        return $i === false ? $default : $i;
     }
 
     public static function l(array $keys)
index 4560f6e..e5187e4 100644 (file)
@@ -23,6 +23,7 @@ define('PL_DO_AUTH',   300);
 define('PL_FORBIDDEN', 403);
 define('PL_NOT_FOUND', 404);
 define('PL_WIKI',      500);
+define('PL_JSON',      501);
 
 abstract class PlHook
 {
@@ -361,7 +362,8 @@ abstract class Platal
 
         try {
             $page->assign('platal', $this);
-            switch ($this->call_hook($page)) {
+            $res = $this->call_hook($page);
+            switch ($res) {
               case PL_FORBIDDEN:
                 $this->mods['core']->handler_403($page);
                 break;
@@ -385,7 +387,11 @@ abstract class Platal
         }
 
         $page->assign('platal', $this);
-        $page->run();
+        if ($res == PL_JSON) {
+            $page->runJSon();
+        } else {
+            $page->run();
+        }
     }
 
     public function error403()
index 9177253..c8e3fd3 100644 (file)
@@ -120,6 +120,23 @@ class PlCache
         return $backend->has($key, $type);
     }
 
+    /** Clear the cache.
+     */
+    private static function clear($type)
+    {
+        $backend = self::getBackend($type);
+        $backend->clear($type);
+    }
+
+
+    /** Clear all the cached data.
+     */
+    public static function clearAll()
+    {
+        self::clearGlobal();
+        self::clearSession();
+        self::clearLocal();
+    }
 
     /** Global data storage. Global data is independent from
      * the current session and can thus be shared by several
@@ -153,6 +170,11 @@ class PlCache
         return self::has($key, self::TIMER);
     }
 
+    public static function clearGlobal()
+    {
+        return self::clear(self::TIMER);
+    }
+
 
     /** Session data storage. Session data is session-dependent
      * and thus must not be shared between sessions but can
@@ -179,6 +201,11 @@ class PlCache
         return self::has($key, self::SESSION);
     }
 
+    public static function clearSession()
+    {
+        return self::clear(self::SESSION);
+    }
+
 
     /** Script local data storage. This stores data that
      * expires at the end of the execution of the current
@@ -204,6 +231,11 @@ class PlCache
     {
         return self::has($key, self::SCRIPT);
     }
+
+    public static function clearLocal()
+    {
+        return self::clear(self::SCRIPT);
+    }
 }
 
 
@@ -244,6 +276,10 @@ interface PlCacheBackend
     /** Remove the entry from the cache.
      */
     public function invalidate($key, $type);
+
+    /** Remove all the entries of the given type from the cache.
+     */
+    public function clear($type);
 }
 
 class PlDummyCache implements PlCacheBackend
@@ -269,6 +305,10 @@ class PlDummyCache implements PlCacheBackend
     public function invalidate($key, $type)
     {
     }
+
+    public function clear($type)
+    {
+    }
 }
 
 abstract class PlArrayCache implements PlCacheBackend
@@ -359,6 +399,11 @@ class PlStaticCache extends PlArrayCache
     {
         unset($this->data[$key]);
     }
+
+    public function clear($type)
+    {
+        $this->data = array();
+    }
 }
 
 class PlSessionCache extends PlArrayCache
@@ -367,9 +412,14 @@ class PlSessionCache extends PlArrayCache
     {
     }
 
+    private function prefix($type)
+    {
+        return '__cache_' . $type . '_';
+    }
+
     protected function arrayKey($key, $type)
     {
-        return '__cache_' . $key;
+        return $this->prefix($type) . $key;
     }
 
     public function get($key, $type, $callback, $cbargs, $expire)
@@ -388,6 +438,16 @@ class PlSessionCache extends PlArrayCache
     {
         S::kill($this->arrayKey($key, $type));
     }
+
+    public function clear($type)
+    {
+        $prefix = $this->prefix($type);
+        foreach ($_SESSION as $key=>$value) {
+            if (starts_with($key, $prefix)) {
+                unset($_SESSION[$key]);
+            }
+        }
+    }
 }
 
 class PlMemcacheCache implements PlCacheBackend
@@ -436,6 +496,11 @@ class PlMemcacheCache implements PlCacheBackend
     {
         return $this->context->delete($key);
     }
+
+    public function clear($type)
+    {
+        return $this->context->flush();
+    }
 }
 
 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
index 2e1bc11..13f6c47 100644 (file)
@@ -39,6 +39,16 @@ class PlDBNoSuchFieldException extends PlException
     }
 }
 
+class PlDBNoSuchKeyException extends PlException
+{
+    public function __construct($key, PlDBTable $table)
+    {
+        parent::__construct('Erreur lors de l\'accès à la base de données',
+                            'No such key ' . $key . ' in table ' . $table->table);
+    }
+}
+
+
 class PlDBIncompleteEntryDescription extends PlException
 {
     public function __construct($field, PlDBTable $table)
@@ -55,8 +65,6 @@ class PlDBTableField
 
     public $name;
     public $inPrimaryKey;
-    public $inUniqueKey;
-    public $inKey;
 
     public $type;
     public $typeLength;
@@ -66,6 +74,9 @@ class PlDBTableField
     public $defaultValue;
     public $autoIncrement;
 
+    private $validator;
+    private $formatter;
+
     public function __construct(array $column)
     {
         $this->name = $column['Field'];
@@ -80,9 +91,7 @@ class PlDBTableField
         }
         $this->allowNull = ($column['Null'] === 'YES');
         $this->autoIncrement = (strpos($column['Extra'], 'auto_increment') !== false);
-        $this->inPrimaryKey = ($column['Key'] === 'PRI');
-        $this->inUniqueKey = $this->inPrimaryKey || ($column['Key'] === 'UNI');
-        $this->inKey = $this->inUniqueKey || ($column['Key'] === 'MUL');
+        $this->inPrimaryKey  = ($column['Key'] == 'PRI');
 
         try {
             $this->defaultValue = $this->format($column['Default']);
@@ -91,6 +100,16 @@ class PlDBTableField
         }
     }
 
+    public function registerFormatter($class)
+    {
+        $this->formatter = $class;
+    }
+
+    public function registerValidator($class)
+    {
+        $this->validator = $class;
+    }
+
     public function format($value, $badNullFallbackToDefault = false)
     {
         if (is_null($value)) {
@@ -101,6 +120,14 @@ class PlDBTableField
                 return $this->defaultValue;
             }
             throw new PlDBBadValueException($value, $this, 'null not allowed');
+        }
+        if (!is_null($this->validator)) {
+            $class = $this->validator;
+            new $class($this, $value);
+        }
+        if (!is_null($this->formatter)) {
+            $class = $this->formatter;
+            $value = new $class($this, $value);
         } else if ($this->type == 'enum') {
             if (!$this->typeParameters->hasFlag($value)) {
                 throw new PlDBBadValueException($value, $this, 'invalid value for enum ' . $this->typeParameters->flags());
@@ -126,32 +153,135 @@ class PlDBTableField
             }
             /* TODO: Check bounds */
             return $value;
-        } else if ($this->type == 'varchar') {
+        } else if (ends_with($this->type, 'char')) {
             if (strlen($value) > $this->typeLength) {
                 throw new PlDBBadValueException($value, $this, 'value is expected to be at most ' . $this->typeLength . ' characters long, ' . strlen($value) . ' given');
             }
             return $value;
-        } else if ($this->type == 'char') {
-            if (strlen($value) != $this->typeLength) {
-                throw new PlDBBadValueException($value, $this, 'value is expected to be ' . $this->typeLength . ' characters long, ' . strlen($value) . ' given');
-            }
-            return $value;
+        } else if (starts_with($this->type, 'date') || $this->type == 'timestamp') {
+            return new DateFieldFormatter($this, $value);
         }
-        /* TODO: Support data and times */
         return $value;
     }
 }
 
+interface PlDBTableFieldValidator
+{
+    public function __construct(PlDBTableField $field, $value);
+}
+
+interface PlDBTableFieldFormatter extends PlDBTableFieldValidator, XDBFormat, PlExportable
+{
+}
+
+class DateFieldFormatter implements PlDBTableFieldFormatter
+{
+    private $datetime;
+    private $storageFormat;
+
+    public function __construct(PlDBTableField $field, $date)
+    {
+        $this->datetime = make_datetime($date);
+        if (is_null($this->datetime)) {
+            throw new PlDBBadValueException($date, $field, 'value is expected to be a date/time, ' . $date . ' given');
+        }
+        if ($field->type == 'date') {
+            $this->storageFormat = 'Y-m-d';
+        } else if ($field->type == 'datetime') {
+            $this->storageFormat = 'Y-m-d H:i:s';
+        } else {
+            $this->storageFormat = 'U';
+        }
+    }
+
+    public function format()
+    {
+        return XDB::escape($this->export());
+    }
+
+    public function date($format)
+    {
+        return $this->datetime->format($format);
+    }
+
+    public function export()
+    {
+        return $this->datetime->format($this->storageFormat);
+    }
+}
+
+class JSonFieldFormatter implements PlDBTableFieldFormatter, ArrayAccess
+{
+    private $data;
+
+    public function __construct(PlDBTableField $field, $data)
+    {
+        if (strpos($field->type, 'text') === false) {
+            throw new PlDBBadValueException($data, $field, 'json formatting requires a text field');
+        }
+
+        if (is_string($data)) {
+            $this->data = json_decode($data, true);
+        } else if (is_object($data)) {
+            if ($data instanceof PlExportable) {
+                $this->data = $data->export();
+            } else {
+                $this->data = json_decode(json_encode($data), true);
+            }
+        } else if (is_array($data)) {
+            $this->data = $data;
+        }
+
+        if (is_null($this->data)) {
+            throw new PlDBBadValueException($data, $field, 'cannot interpret data as json: ' . $data);
+        }
+    }
+
+    public function format()
+    {
+        return XDB::escape(json_encode($this->data));
+    }
+
+    public function export()
+    {
+        return $this->data;
+    }
+
+    public function offsetExists($offset)
+    {
+        return isset($this->data[$offset]);
+    }
+
+    public function offsetGet($offset)
+    {
+        return $this->data[$offset];
+    }
+
+    public function offsetSet($offset, $value)
+    {
+        $this->data[$offset] = $value;
+    }
+
+    public function offsetUnset($offset)
+    {
+        unset($this->data[$offset]);
+    }
+}
+
 
 /** This class aims at providing a simple interface to interact with a single
  * table of a database. It is implemented as a wrapper around XDB.
  */
 class PlDBTable
 {
+    const PRIMARY_KEY = 'PRIMARY';
+
     public $table;
 
     private $schema;
-    private $keyFields;
+    private $primaryKey;
+    private $uniqueKeys;
+    private $multipleKeys;
     private $mutableFields;
 
     public function __construct($table)
@@ -160,20 +290,38 @@ class PlDBTable
         $this->schema();
     }
 
-    private function parseSchema(PlIterator $schema)
+    private function parseSchema(PlIterator $schema, PlIterator $keys)
     {
         $this->schema = array();
-        $this->keyFields = array();
+        $this->primaryKey = array();
+        $this->uniqueKeys = array();
+        $this->multipleKeys = array();
         $this->mutableFields = array();
         while ($column = $schema->next()) {
             $field = new PlDBTableField($column);
             $this->schema[$field->name] = $field;
-            if ($field->inPrimaryKey) {
-                $this->keyFields[] = $field->name;
-            } else {
+            if (!$field->inPrimaryKey) {
                 $this->mutableFields[] = $field->name;
             }
         }
+        while ($column = $keys->next()) {
+            $name     = $column['Key_name'];
+            $multiple = intval($column['Non_unique']) != 0;
+            $field    = $column['Column_name'];
+            if ($multiple) {
+                if (!isset($this->multipleKeys[$name])) {
+                    $this->multipleKeys[$name] = array();
+                }
+                $this->multipleKeys[$name][] = $field;
+            } else if ($name == self::PRIMARY_KEY) {
+                $this->primaryKey[] = $field;
+            } else {
+                if (!isset($this->uniqueKeys[$name])) {
+                    $this->uniqueKeys[$name] = array();
+                }
+                $this->uniqueKeys[$name][] = $field;
+            }
+        }
     }
 
 
@@ -181,7 +329,8 @@ class PlDBTable
     {
         if (!$this->schema) {
             $schema = XDB::iterator('DESCRIBE ' . $this->table);
-            $this->parseSchema($schema);
+            $keys   = XDB::iterator('SHOW INDEX FROM ' . $this->table);
+            $this->parseSchema($schema, $keys);
         }
         return $this->schema;
     }
@@ -200,15 +349,68 @@ class PlDBTable
         return $this->field($field)->format($value);
     }
 
+    public function registerFieldFormatter($field, $class)
+    {
+        return $this->field($field)->registerFormatter($class);
+    }
+
+    public function registerFieldValidator($field, $class)
+    {
+        return $this->field($field)->registerValidator($class);
+    }
+
+
     public function defaultValue($field)
     {
         return $this->field($field)->defaultValue;
     }
 
-    public function primaryKey(PlDBTableEntry $entry)
+    private function hasKeyField(PlDBTableEntry $entry, array $fields)
+    {
+        foreach ($fields as $field) {
+            if (isset($entry->$field)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private function keyFields($keyName)
+    {
+        if ($keyName == self::PRIMARY_KEY) {
+            return $this->primaryKey;
+        } else if (isset($this->uniqueKeys[$keyName])) {
+            return $this->uniqueKeys[$keyName];
+        } else if (isset($this->multipleKeys[$keyName])) {
+            return $this->multipleKeys[$keyName];
+        }
+        throw new PlDBNoSuchKeyException($keyName, $this);
+    }
+
+    private function bestKeyFields(PlDBTableEntry $entry, $allowMultiple)
+    {
+        if ($this->hasKeyField($entry, $this->primaryKey)) {
+            return $this->primaryKey;
+        }
+        foreach ($this->uniqueKeys as $fields) {
+            if ($this->hasKeyField($entry, $fields)) {
+                return $fields;
+            }
+        }
+        if ($allowMultiple) {
+            foreach ($this->multipleKeys as $fields) {
+                if ($this->hasKeyField($entry, $fields)) {
+                    return $fields;
+                }
+            }
+        }
+        return $this->primaryKey;
+    }
+
+    public function key(PlDBTableEntry $entry, array $keyFields)
     {
         $key = array();
-        foreach ($this->keyFields as $field) {
+        foreach ($keyFields as $field) {
             if (!isset($entry->$field)) {
                 throw new PlDBIncompleteEntryDescription($field, $this);
             } else {
@@ -218,10 +420,15 @@ class PlDBTable
         return implode('-', $key);
     }
 
-    private function buildKeyCondition(PlDBTableEntry $entry, $allowIncomplete)
+    public function primaryKey(PlDBTableEntry $entry)
+    {
+        return $this->key($this->keyFields(self::PRIMARY_KEY));
+    }
+
+    private function buildKeyCondition(PlDBTableEntry $entry, array $keyFields, $allowIncomplete)
     {
         $condition = array();
-        foreach ($this->keyFields as $field) {
+        foreach ($keyFields as $field) {
             if (!isset($entry->$field)) {
                 if (!$allowIncomplete) {
                     throw new PlDBIncompleteEntryDescription($field, $this);
@@ -237,44 +444,146 @@ class PlDBTable
     {
         $result = XDB::rawFetchOneAssoc('SELECT  *
                                            FROM  ' . $this->table . '
-                                          WHERE  ' . $this->buildKeyCondition($entry, false));
+                                          WHERE  ' . $this->buildKeyCondition($entry,
+                                                                              $this->bestKeyFields($entry, false),
+                                                                              false));
         if (!$result) {
             return false;
         }
         return $entry->fillFromDBData($result);
     }
 
-    public function iterateOnEntry(PlDBTableEntry $entry)
+    public function iterateOnCondition(PlDBTableEntry $entry, $condition, $sortField)
     {
+        if (empty($sortField)) {
+            $sortField = $this->primaryKey;
+        }
+        if (!is_array($sortField)) {
+            $sortField = array($sortField);
+        }
+        $sort = ' ORDER BY ' . implode(', ', $sortField);
         $it = XDB::rawIterator('SELECT  *
                                   FROM  ' . $this->table . '
-                                 WHERE  ' . $this->buildKeyCondition($entry, true));
+                                 WHERE  ' . $condition . '
+                                        ' . $sort);
         return PlIteratorUtils::map($it, array($entry, 'cloneAndFillFromDBData'));
     }
 
-    public function updateEntry(PlDBTableEntry $entry)
+    public function iterateOnEntry(PlDBTableEntry $entry, $sortField)
     {
-        $values = array();
-        foreach ($this->mutableFields as $field) {
-            if ($entry->hasChanged($field)) {
-                $values[] = XDB::format($field . ' = {?}', $entry->$field);
+        return $this->iterateOnCondition($entry,
+                                         $this->buildKeyCondition($entry,
+                                                                  $this->bestKeyFields($entry, true),
+                                                                  true),
+                                         $sortField);
+    }
+
+    const SAVE_INSERT_MISSING   = 0x01;
+    const SAVE_UPDATE_EXISTING  = 0x02;
+    const SAVE_IGNORE_DUPLICATE = 0x04;
+    public function saveEntries(array $entries, $flags)
+    {
+        $flags &= (self::SAVE_INSERT_MISSING | self::SAVE_UPDATE_EXISTING | self::SAVE_IGNORE_DUPLICATE);
+        Platal::assert($flags != 0, "Hey, the flags ($flags) here are so stupid, don't know what to do");
+        if ($flags == self::SAVE_UPDATE_EXISTING) {
+            foreach ($entries as $entry) {
+                $values = array();
+                foreach ($this->mutableFields as $field) {
+                    if ($entry->hasChanged($field)) {
+                        $values[] = XDB::format($field . ' = {?}', $entry->$field);
+                    }
+                }
+                if (count($values) > 0) {
+                    XDB::rawExecute('UPDATE ' . $this->table . '
+                                        SET ' . implode(', ', $values) . '
+                                      WHERE ' . $this->buildKeyCondition($entry,
+                                                                         $this->keyFields(self::PRIMARY_KEY),
+                                                                         false));
+                }
+            }
+        } else {
+            $fields = new PlFlagSet();
+            foreach ($entries as $entry) {
+                foreach ($this->schema as $field=>$type) {
+                    if ($type->inPrimaryKey || $entry->hasChanged($field)) {
+                        $fields->addFlag($field);
+                    }
+                }
+            }
+            if (count($fields->export()) > 0) {
+                foreach ($entries as $entry) {
+                    $v = array();
+                    foreach ($fields as $field) {
+                        $v[$field] = XDB::escape($entry->$field);
+                    }
+                    $values[] = '(' . implode(', ', $v) . ')';
+                }
+
+                $query = $this->table . ' (' . implode(', ', $fields->export()) . ')
+                               VALUES ' . implode(",\n", $values);
+                if (($flags & self::SAVE_UPDATE_EXISTING)) {
+                    $update = array();
+                    foreach ($this->mutableFields as $field) {
+                        if (isset($values[$field])) {
+                            $update[] = "$field = VALUES($field)";
+                        }
+                    }
+                    if (count($update) > 0) {
+                        $query = 'INSERT INTO ' . $query;
+                        $query .= "\n  ON DUPLICATE KEY UPDATE " . implode(', ', $update);
+                    } else {
+                        $query = 'INSERT IGNORE INTO ' . $query;
+                    }
+                } else if (($flags & self::SAVE_IGNORE_DUPLICATE)) {
+                    $query = 'INSERT IGNORE INTO ' . $query;
+                } else {
+                    $query = 'INSERT INTO ' . $query;
+                }
+                XDB::rawExecute($query);
+                if (count($entries) == 1) {
+                    $id = XDB::insertId();
+                    if ($id) {
+                        $entry = end($entries);
+                        foreach ($this->primaryKey as $field) {
+                            if ($this->schema[$field]->autoIncrement) {
+                                $entry->$field = $id;
+                                break;
+                            }
+                        }
+                    }
+                }
             }
         }
-        if (count($values) > 0) {
-            XDB::rawExecute('UPDATE ' . $this->table . '
-                                SET ' . implode(', ', $values) . '
-                              WHERE ' . $this->buildKeyCondition($entry, false));
+    }
+
+    public function deleteEntry(PlDBTableEntry $entry, $allowIncomplete)
+    {
+        XDB::rawExecute('DELETE FROM ' . $this->table . '
+                               WHERE ' . $this->buildKeyCondition($entry,
+                                                                  $this->bestKeyFields($entry, $allowIncomplete),
+                                                                  $allowIncomplete));
+    }
+
+    public function exportEntry(PlDBTableEntry $entry)
+    {
+        $export = array();
+        foreach ($this->schema as $key=>$field) {
+            $value = $entry->$key;
+            if ($value instanceof PlExportable) {
+                $value = $value->export();
+            }
+            $export[$key] = $value;
         }
+        return $export;
     }
 
     public static function get($name)
     {
-        var_dump('blah');
         return new PlDBTable($name);
     }
 }
 
-class PlDBTableEntry extends PlAbstractIterable
+class PlDBTableEntry extends PlAbstractIterable implements PlExportable
 {
     private $table;
     private $changed;
@@ -294,6 +603,28 @@ class PlDBTableEntry extends PlAbstractIterable
         $this->changed = new PlFlagSet();
     }
 
+    /** Register a custom formatter for a field.
+     *
+     * A formatter can be used to perform on-the-fly conversion from db storage to a user-friendly format.
+     * For example, if you have a textual field that contain json, you can use a JSonFieldFormatter on this
+     * field to perform automatic decoding when reading from the database (or when assigning the field)
+     * and automatic json_encoding when storing the object back to the db.
+     */
+    protected function registerFieldFormatter($field, $formatterClass)
+    {
+        $this->table->registerFieldFormatter($field, $formatterClass);
+    }
+
+    /** Register a custom validator for a field.
+     *
+     * A validator perform a pre-filter on the value of a field. As opposed to the formatters, it does
+     * not affects how the value is stored in the database.
+     */
+    protected function registerFieldValidator($field, $validatorClass)
+    {
+        $this->table->registerFieldValidator($field, $validatorClass);
+    }
+
     /** This hook is called when the entry is going to be updated in the db.
      *
      * A typical usecase is a class that stores low-level representation of
@@ -307,6 +638,26 @@ class PlDBTableEntry extends PlAbstractIterable
         return true;
     }
 
+    /** This hook is called when the entry has been save in the database.
+     *
+     * It can be used to perform post-actions on save like storing extra data
+     * in database or sending a notification.
+     */
+    protected function postSave()
+    {
+    }
+
+    /** This hook is called when the entry is going to be deleted from the db.
+     *
+     * Default behavior is to call preSave().
+     *
+     * @return true in case of success.
+     */
+    protected function preDelete()
+    {
+        return $this->preSave();
+    }
+
     /** This hook is called when the entry has just been fetched from the db.
      *
      * This is the counterpart of @ref preSave and a typical use-case is the conversion
@@ -368,6 +719,15 @@ class PlDBTableEntry extends PlAbstractIterable
         return $this->postFetch();
     }
 
+    public function copy(PlDBTableEntry $other)
+    {
+        Platal::assert($this->table == $other->table,
+                       "Trying to fill an entry of table {$this->table->table} with content of {$other->table->table}.");
+        $this->changed = $other->changed;
+        $this->fetched = $other->fetched;
+        $this->data    = $other->data;
+    }
+
     public function cloneAndFillFromDBData(array $data)
     {
         $clone = clone $this;
@@ -380,20 +740,81 @@ class PlDBTableEntry extends PlAbstractIterable
         return $this->table->fetchEntry($this);
     }
 
-    public function iterate()
+    public function iterate($sortField = null)
     {
-        return $this->table->iterateOnEntry($this);
+        return $this->table->iterateOnEntry($this, $sortField);
     }
 
-    public function save()
+    public function iterateOnCondition($condition, $sortField = null)
     {
-        if (!$this->preSave()) {
-            return false;
+        return $this->table->iterateOnCondition($this, $condition, $sortField);
+    }
+
+    public function save($flags)
+    {
+        return self::saveBatch(array($this), $flags);
+    }
+
+    public function update($insertMissing = false)
+    {
+        $flags = PlDBTable::SAVE_UPDATE_EXISTING;
+        if ($insertMissing) {
+            $flags = PlDBTable::SAVE_INSERT_MISSING;
+        }
+        return $this->save($flags);
+    }
+
+    public function insert($allowUpdate = false)
+    {
+        $flags = PlDBTable::SAVE_INSERT_MISSING;
+        if ($allowUpdate) {
+            $flags |= PlDBTable::SAVE_UPDATE_EXISTING;
+        }
+        return $this->save($flags);
+    }
+
+    public function delete()
+    {
+        if (!$this->preDelete()) {
+            return 0;
+        }
+        return $this->table->deleteEntry($this, true);
+    }
+
+    public function export()
+    {
+        return $this->table->exportEntry($this);
+    }
+
+    protected static function saveBatch($entries, $flags)
+    {
+        $table = null;
+        foreach ($entries as $entry) {
+            if (is_null($table)) {
+                $table = $entry->table;
+            } else {
+                Platal::assert($table === $entry->table, "Cannot save batch of entries of different kinds");
+            }
+            if (!$entry->preSave()) {
+                return false;
+            }
+        }
+        $table->saveEntries($entries, $flags);
+        foreach ($entries as $entry) {
+            $entry->changed->clear();
+            $entry->postSave();
         }
-        $this->table->updateEntry($this);
-        $this->changed->clear();
         return true;
     }
+
+    public static function insertBatch($entries, $allowUpdate = false)
+    {
+        $flags = PlDBTable::SAVE_INSERT_MISSING;
+        if ($allowUpdate) {
+            $flags |= PlDBTable::SAVE_UPDATE_EXISTING;
+        }
+        return self::saveBatch($entries, $flags);
+    }
 }
 
 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
index b67095a..21a911b 100644 (file)
@@ -79,8 +79,8 @@ class PlDict
 
     public function i($key, $default = 0)
     {
-        $i = $this->_get($key, $default);
-        return (is_int($i) || ctype_digit($i)) ? intval($i) : $default;
+        $i = to_integer($this->_get($key, $default));
+        return $i === false ? $default : $i;
     }
 
     public function l(array $keys)
index 520acf0..194cd1b 100644 (file)
@@ -74,7 +74,12 @@ class PlErrorReportIterator implements PlIterator
 
     public function __construct()
     {
-        $this->file = fopen(Platal::globals()->spoolroot . '/spool/tmp/site_errors', 'r');
+        $file = Platal::globals()->spoolroot . '/spool/tmp/site_errors';
+        if (file_exists($file)) {
+            $this->file = fopen($file, 'r');
+        } else {
+            $this->file = null;
+        }
     }
 
     public function next()
diff --git a/classes/plexportable.php b/classes/plexportable.php
new file mode 100644 (file)
index 0000000..0f1545d
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+/***************************************************************************
+ *  Copyright (C) 2003-2010 Polytechnique.org                              *
+ *  http://opensource.polytechnique.org/                                   *
+ *                                                                         *
+ *  This program is free software; you can redistribute it and/or modify   *
+ *  it under the terms of the GNU General Public License as published by   *
+ *  the Free Software Foundation; either version 2 of the License, or      *
+ *  (at your option) any later version.                                    *
+ *                                                                         *
+ *  This program is distributed in the hope that it will be useful,        *
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of         *
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
+ *  GNU General Public License for more details.                           *
+ *                                                                         *
+ *  You should have received a copy of the GNU General Public License      *
+ *  along with this program; if not, write to the Free Software            *
+ *  Foundation, Inc.,                                                      *
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
+ ***************************************************************************/
+
+// {{{ interface PlExportable
+/** PlExportable intends to enable a robust exportation of objects.
+ *  By explicitly implementing the exportation process, PlExportable classes are
+ *  able to be rebuilt from their exportation, even if the class has been modified
+ *  inbetween, a case that classic php serialization badly handle.
+ */
+interface PlExportable
+{
+    /** Returns an associative array containing the neccessary
+     *  datas to rebuild the instance. The result can then be serialized,
+     *  for example thank the json_encode() function.
+     */
+    public function export();
+}
+// }}}
+
+// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
+?>
index af8141d..5dd77e6 100644 (file)
@@ -53,7 +53,7 @@ class PlLimit
  *     descending order).
  * The getSortTokens function is used to get actual ordering part of the query.
  */
-abstract class PlFilterOrder
+abstract class PlFilterOrder implements PlExportable
 {
     protected $desc = false;
     public function __construct($desc = false)
@@ -62,6 +62,20 @@ abstract class PlFilterOrder
         $this->_tokens = null;
     }
 
+    protected function buildExport($type)
+    {
+        $export = array('type' => $type);
+        if ($this->desc) {
+            $export['order'] = 'desc';
+        }
+        return $export;
+    }
+
+    public function export()
+    {
+        throw new Exception("This instance is not exportable");
+    }
+
     public function toggleDesc()
     {
         $this->desc = !$this->desc;
@@ -72,7 +86,7 @@ abstract class PlFilterOrder
         $this->desc = $desc;
     }
 
-    public function buildSort(PlFilter &$pf)
+    public function buildSort(PlFilter $pf)
     {
         $sel = $this->getSortTokens($pf);
         $this->_tokens = $sel;
@@ -88,10 +102,10 @@ abstract class PlFilterOrder
     }
 
     /** This function must return the tokens to use for ordering
-     * @param &$pf The PlFilter whose results must be ordered
+     * @param $pf The PlFilter whose results must be ordered
      * @return The name of the field to use for ordering results
      */
-    abstract protected function getSortTokens(PlFilter &$pf);
+    abstract protected function getSortTokens(PlFilter $pf);
 }
 // }}}
 
@@ -105,7 +119,7 @@ abstract class PlFilterGroupableOrder extends PlFilterOrder
      * the returned token will be used to group the values.
      * It will always be called AFTER getSortTokens().
      */
-    public function getGroupToken(PlFilter &$pf)
+    public function getGroupToken(PlFilter $pf)
     {
         return $this->_tokens;
     }
@@ -123,7 +137,7 @@ class PFO_Random extends PlFilterOrder
         $this->seed = $seed;
     }
 
-    protected function getSortTokens(PlFilter &$pf)
+    protected function getSortTokens(PlFilter $pf)
     {
         if ($this->seed == null) {
             return 'RAND()';
@@ -131,16 +145,24 @@ class PFO_Random extends PlFilterOrder
             return XDB::format('RAND({?})', $this->seed);
         }
     }
+
+    public function export()
+    {
+        $export = array('type' => 'random',);
+        if ($this->seed !== null)
+            $export['seed'] = $this->seed;
+        return $export;
+    }
 }
 // }}}
 
 // {{{ interface PlFilterCondition
-interface PlFilterCondition
+interface PlFilterCondition extends PlExportable
 {
     const COND_TRUE  = 'TRUE';
     const COND_FALSE = 'FALSE';
 
-    public function buildCondition(PlFilter &$pf);
+    public function buildCondition(PlFilter $pf);
 }
 // }}}
 
@@ -149,16 +171,21 @@ abstract class PFC_OneChild implements PlFilterCondition
 {
     protected $child;
 
-    public function __construct(&$child = null)
+    public function __construct($child = null)
     {
         if (!is_null($child) && ($child instanceof PlFilterCondition)) {
             $this->setChild($child);
         }
     }
 
-    public function setChild(PlFilterCondition &$cond)
+    public function setChild(PlFilterCondition $cond)
+    {
+        $this->child = $cond;
+    }
+
+    public function export()
     {
-        $this->child =& $cond;
+        return array('child' => $child->export());
     }
 }
 // }}}
@@ -175,16 +202,16 @@ abstract class PFC_NChildren implements PlFilterCondition
 
     public function addChildren(array $conds)
     {
-        foreach ($conds as &$cond) {
+        foreach ($conds as $cond) {
             if (!is_null($cond) && ($cond instanceof PlFilterCondition)) {
                 $this->addChild($cond);
             }
         }
     }
 
-    public function addChild(PlFilterCondition &$cond)
+    public function addChild(PlFilterCondition $cond)
     {
-        $this->children[] =& $cond;
+        $this->children[] = $cond;
     }
 
     protected function catConds(array $cond, $op, $fallback)
@@ -197,33 +224,52 @@ abstract class PFC_NChildren implements PlFilterCondition
             return '(' . implode(') ' . $op . ' (', $cond) . ')';
         }
     }
+
+    public function export()
+    {
+        $export = array();
+        foreach ($this->children as $child) {
+            $export[] = $child->export();
+        }
+        return array('children' => $export);
+    }
 }
 // }}}
 
 // {{{ class PFC_True
 class PFC_True implements PlFilterCondition
 {
-    public function buildCondition(PlFilter &$uf)
+    public function buildCondition(PlFilter $uf)
     {
         return self::COND_TRUE;
     }
+
+    public function export()
+    {
+        return array('type' => 'true');
+    }
 }
 // }}}
 
 // {{{ class PFC_False
 class PFC_False implements PlFilterCondition
 {
-    public function buildCondition(PlFilter &$uf)
+    public function buildCondition(PlFilter $uf)
     {
         return self::COND_FALSE;
     }
+
+    public function export()
+    {
+        return array('type' => 'false');
+    }
 }
 // }}}
 
 // {{{ class PFC_Not
 class PFC_Not extends PFC_OneChild
 {
-    public function buildCondition(PlFilter &$uf)
+    public function buildCondition(PlFilter $uf)
     {
         $val = $this->child->buildCondition($uf);
         if ($val == self::COND_TRUE) {
@@ -234,20 +280,27 @@ class PFC_Not extends PFC_OneChild
             return 'NOT (' . $val . ')';
         }
     }
+
+    public function export()
+    {
+        $export = parent::export();
+        $export['type'] = 'not';
+        return $export;
+    }
 }
 // }}}
 
 // {{{ class PFC_And
 class PFC_And extends PFC_NChildren
 {
-    public function buildCondition(PlFilter &$uf)
+    public function buildCondition(PlFilter $uf)
     {
         if (empty($this->children)) {
             return self::COND_FALSE;
         } else {
             $true = self::COND_FALSE;
             $conds = array();
-            foreach ($this->children as &$child) {
+            foreach ($this->children as $child) {
                 $val = $child->buildCondition($uf);
                 if ($val == self::COND_TRUE) {
                     $true = self::COND_TRUE;
@@ -260,20 +313,26 @@ class PFC_And extends PFC_NChildren
             return $this->catConds($conds, 'AND', $true);
         }
     }
+
+    public function export() {
+        $export = parent::export();
+        $export['type'] = 'and';
+        return $export;
+    }
 }
 // }}}
 
 // {{{ class PFC_Or
 class PFC_Or extends PFC_NChildren
 {
-    public function buildCondition(PlFilter &$uf)
+    public function buildCondition(PlFilter $uf)
     {
         if (empty($this->children)) {
             return self::COND_TRUE;
         } else {
             $true = self::COND_TRUE;
             $conds = array();
-            foreach ($this->children as &$child) {
+            foreach ($this->children as $child) {
                 $val = $child->buildCondition($uf);
                 if ($val == self::COND_TRUE) {
                     return self::COND_TRUE;
@@ -286,11 +345,17 @@ class PFC_Or extends PFC_NChildren
             return $this->catConds($conds, 'OR', $true);
         }
     }
+
+    public function export() {
+        $export = parent::export();
+        $export['type'] = 'or';
+        return $export;
+    }
 }
 // }}}
 
 // {{{ class PlFilter
-abstract class PlFilter
+abstract class PlFilter implements PlExportable
 {
     /** Filters objects matching the PlFilter
      * @param $objects The objects to filter
@@ -298,9 +363,9 @@ abstract class PlFilter
      */
     public abstract function filter(array $objects, $limit = null);
 
-    public abstract function setCondition(PlFilterCondition &$cond);
+    public abstract function setCondition(PlFilterCondition $cond);
 
-    public abstract function addSort(PlFilterOrder &$sort);
+    public abstract function addSort(PlFilterOrder $sort);
 
     public abstract function getTotalCount();
 
index 6e5a039..7957075 100644 (file)
@@ -21,7 +21,7 @@
 
 /** class for describing flags
  */
-class PlFlagSet extends PlAbstractIterable implements XDBFormat
+class PlFlagSet extends PlAbstractIterable implements XDBFormat, PlExportable
 {
     /** string that holds the PlFlagSet */
     private $values = array();
@@ -128,6 +128,18 @@ class PlFlagSet extends PlAbstractIterable implements XDBFormat
         return $flags;
     }
 
+    /** export the PlFlagSet
+     */
+    public function export()
+    {
+        $array = array();
+        foreach ($this->values as $key=>$value) {
+            if ($value) {
+                $array[] = $key;
+            }
+        }
+        return $array;
+    }
 
     /** format for XDB
      */
@@ -141,13 +153,7 @@ class PlFlagSet extends PlAbstractIterable implements XDBFormat
      */
     public function iterate()
     {
-        $array = array();
-        foreach ($this->values as $key=>$value) {
-            if ($value) {
-                $array[] = $key;
-            }
-        }
-        return PlIteratorUtils::fromArray($array, 1, true);
+        return PlIteratorUtils::fromArray($this->export(), 1, true);
     }
 }
 
index 8cbbf13..55442d0 100644 (file)
@@ -165,6 +165,7 @@ abstract class PlPage extends Smarty
         if (!$globals->debug) {
             error_reporting(0);
             $this->display($skin);
+            pl_print_errors(true);
             exit;
         }
 
@@ -371,9 +372,8 @@ abstract class PlPage extends Smarty
     protected function jsonDisplay()
     {
         pl_content_headers("text/javascript");
-        if (!empty($GLOBALS['pl_errors'])) {
-            $this->jsonAssign('pl_errors', join("\n", $GLOBALS['pl_errors']));
-            $GLOBALS['pl_errors'] = array();
+        if (!empty(PlBacktrace::$bt)) {
+            $this->jsonAssign('pl_backtraces', PlBacktrace::$bt);
         }
         array_walk_recursive($this->_jsonVars, "escape_XDB");
         $jsonbegin = Env::v('jsonBegin');
@@ -389,6 +389,17 @@ abstract class PlPage extends Smarty
         exit;
     }
     // }}}
+
+    public function runJSon()
+    {
+        pl_content_headers("text/javascript");
+        if (!empty(PlBacktrace::$bt)) {
+            $this->jsonAssign('pl_backtraces', PlBacktrace::$bt);
+        }
+        echo json_encode($this->_jsonVars);
+        exit;
+    }
+
     // {{{ function jsonAssign
     public function jsonAssign($var, $value)
     {
index d436bf1..456736f 100644 (file)
@@ -56,8 +56,8 @@ class S
 
     public static function i($key, $default = 0)
     {
-        $i = S::v($key, $default);
-        return is_numeric($i) ? intval($i) : $default;
+        $i = to_integer(S::v($key, $default));
+        return $i === false ? $default : $i;
     }
 
     public static function t($key, $default = '')
index bfa5163..ad37a92 100644 (file)
@@ -204,7 +204,18 @@ function make_datetime($date)
         return new DateTime("@$date");
     } else {
         try {
+            $list = explode('/', $date);
+            if (count($list) == 3) {
+                $date = $list[1] . '/' . $list[0] . '/' . $list[2];
+            }
+            // FIXME: On PHP < 5.3, parsing error are reported using an error,
+            //        not an exception. Thus count the number of error to detect
+            //        errors.
+            $errors = @count($GLOBALS['pl_errors']);
             $d = new DateTime($date);
+            if (@count($GLOBALS['pl_errors']) > $errors) {
+                return null;
+            }
             return $d;
         } catch (Exception $e) {
             return null;
@@ -369,6 +380,23 @@ function ends_with($string, $suffix, $caseSensitive = true)
     }
 }
 
+/** Check if the input data can be seen as an integer.
+ */
+function can_convert_to_integer($data)
+{
+    return is_int($data) || ctype_digit($data);
+}
+
+/** Interpret the input data as an integer or return false.
+ */
+function to_integer($data)
+{
+    if (!can_convert_to_integer($data)) {
+        return false;
+    }
+    return intval($data);
+}
+
 
 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
 ?>
index 069a661..a638d0d 100644 (file)
@@ -65,6 +65,7 @@ function pl_core_include($file)
 
 function pl_error_handler($errno, $errstr, $errfile, $errline)
 {
+
     static $errortype;
     if (!error_reporting())
         return;
@@ -93,23 +94,17 @@ function pl_error_handler($errno, $errstr, $errfile, $errline)
             return;
         }
     }
-
     $type = isset($errortype[$errno]) ? $errortype[$errno] : $errno;
-    $errstr = utf8_encode(htmlentities($errstr));
-    if (php_sapi_name() == 'cli') {
-        $GLOBALS['pl_errors'] = "$type: $errstr\n  $errfile:$errline\n";
-    } else {
-        $GLOBALS['pl_errors'][] =
-            "<div class='phperror'>".
-            "<strong>{$type}</strong> <em>$errstr</em><br />".
-            "<tt>$errfile : $errline</tt>".
-            "</div>";
-    }
-}
+    $error = strpos($type, 'Warning') !== false || strpos($type, 'Error') !==false;
 
-function pl_clear_errors()
-{
-    unset($GLOBALS['pl_errors']);
+    pl_autoload('PlBacktrace');
+    if (!isset(PlBacktrace::$bt['PHP Errors'])) {
+        new PlBacktrace('PHP Errors');
+    }
+    PlBacktrace::$bt['PHP Errors']->newEvent("$type: $errstr",
+                                             0, $error ? $errstr : null,
+                                             array(array('file' => $errfile,
+                                                         'line' => $errline)));
 }
 
 function pl_dump_env()
@@ -122,15 +117,27 @@ function pl_dump_env()
     echo "</pre></div>";
 }
 
-function pl_print_errors()
+function pl_print_errors($html = false)
 {
-    if (!empty($GLOBALS['pl_errors'])) {
-        print join("\n", $GLOBALS['pl_errors']);
+    if (!isset(PlBacktrace::$bt['PHP Errors'])) {
+        return;
+    }
+    foreach (PlBacktrace::$bt['PHP Errors']->traces as $trace) {
+        if ($html) {
+            echo "<pre>";
+        }
+        print "{$trace['action']}\n";
+        print "  {$trace['data'][0]['file']}: {$trace['data'][0]['line']}\n";
+        if ($html) {
+            echo "</pre>";
+        }
     }
 }
 
 set_error_handler('pl_error_handler', E_ALL | E_STRICT);
-register_shutdown_function('pl_print_errors');
+if (php_sapi_name() == 'cli') {
+    register_shutdown_function('pl_print_errors');
+}
 //register_shutdown_function('pl_dump_env');
 
 /** Check if the string is utf8
index 4c00f88..6557ca4 100644 (file)
@@ -114,6 +114,7 @@ class CoreModule extends PLModule
 
         $page->clear_compiled_tpl();
         PlWikiPage::clearCache();
+        PlCache::clearAll();
 
         http_redirect(empty($_SERVER['HTTP_REFERER']) ? './' : $_SERVER['HTTP_REFERER']);
     }
@@ -183,11 +184,11 @@ class CoreModule extends PLModule
     function handler_siteerror($page) {
         global $globals;
         $page->coreTpl('site_errors.tpl');
-        $page->assign('errors', PlErrorReport::iterate());
         if (Post::has('clear')) {
             PlErrorReport::clear();
             $page->trigSuccess("Erreurs effacées.");
         }
+        $page->assign('errors', PlErrorReport::iterate());
     }
 
     function handler_embedded($page)