data = new stdClass(); } private static function convertFromStorage($string) { return json_decode($string); } private static function convertToStorage($dataObj) { $obj = clone($dataObj); unset($obj->id); return json_encode($obj); } /* * get new object */ public static function create($data) { if (!is_object($data)) { throw new Exception('data must be an object'); } $properties = array_merge( static::ENCRYPTED_PROPERTIES, static::PLAIN_PROPERTIES, static::SERVER_PROPERTIES ); // check if all properties exist on data object foreach ($properties as $property) { if (!property_exists($data, $property)) { throw new Exception('property ' . $property . ' does not exist on data object'); } } // check that encrypted properties are valid AES foreach (static::ENCRYPTED_PROPERTIES as $encryptedProperty) { if (!self::isValidSJCL($data->$encryptedProperty)) { throw new Exception('property ' . $property . ' is not valid AES'); } } $modelName = get_called_class(); $model = new $modelName; foreach ($properties as $property) { $model->set($property, $data->$property); } return $model; } public function export() { $data = new stdClass(); $publicProperties = array_merge( static::ENCRYPTED_PROPERTIES, static::PLAIN_PROPERTIES, array('id') ); foreach($publicProperties as $publicProperty) { $data->$publicProperty = $this->get($publicProperty); } if (method_exists($this, 'includeRelationships')) { $this->includeRelationships($data); } return $data; } protected function get($property) { if(property_exists($this->data, $property)) { return $this->data->$property; } else { return null; } } protected function getDir() { throw new Exception ('getDir must be implemented by model'); } protected function getPath() { throw new Exception ('getPath must be implemented by model'); } /* * Checks if a json string is a proper SJCL encrypted message. * False if format is incorrect. * * Taken from: https://github.com/sebsauvage/ZeroBin/blob/8cae64d6eab99fb0d31868df77846285c0958ed0/index.php#L76-L109 * Copyright (c) 2012 Sébastien SAUVAGE (sebsauvage.net) * License: https://github.com/sebsauvage/ZeroBin/blob/8cae64d6eab99fb0d31868df77846285c0958ed0/README.md */ private static function isValidSJCL($jsonstring) { $accepted_keys=array('iv','v','iter','ks','ts','mode','adata','cipher','salt','ct'); // Make sure content is valid json $decoded = json_decode($jsonstring); if ($decoded==null) return false; $decoded = (array)$decoded; // Make sure required fields are present foreach($accepted_keys as $k) { if (!array_key_exists($k,$decoded)) { return false; } } // Make sure some fields are base64 data if (base64_decode($decoded['iv'],$strict=true)==null) { return false; } if (base64_decode($decoded['salt'],$strict=true)==null) { return false; } if (base64_decode($decoded['cipher'],$strict=true)==null) { return false; } // Make sure no additionnal keys were added. if (count(array_intersect(array_keys($decoded),$accepted_keys))!=10) { return false; } // Reject data if entropy is too low $ct = base64_decode($decoded['ct'], $strict=true); if (strlen($ct) > strlen(gzdeflate($ct))) return false; // Make sure some fields have a reasonable size. if (strlen($decoded['iv'])>24) return false; if (strlen($decoded['salt'])>14) return false; return true; } /* * restore object from storage */ public static function restore($id) { if (!static::isValidId($id)) { throw new Exception($id . ' is not a valid id'); } $modelName = get_called_class(); $model = new $modelName; $model->set('id', $id); $path = $model->getPath(); if (!is_file($path)) { // no poll with this id return false; } try { $storageObject = file_get_contents( $path ); if ($storageObject === false) { // no poll with this id return false; } } catch (Exception $e) { return false; } $data = self::convertFromStorage($storageObject); if(method_exists($model, 'restoreLegacySupportHook')) { $model->restoreLegacySupportHook($data); } $properties = array_merge( static::ENCRYPTED_PROPERTIES, static::PLAIN_PROPERTIES, static::SERVER_PROPERTIES ); foreach ($properties as $property) { if (isset($data->$property)) { $model->set($property, $data->$property); } } if (method_exists($model, 'restoreHook')) { if ($model->restoreHook() === false) { return false; } } return $model; } /* * save object to storage * gives back new id */ public function save() { // create dir for data if it does not exists $counter = 0; while (true) { $this->set('id', $this->generateNewId()); try { if (!is_dir($this->getDir())) { if (mkdir($this->getDir()) === false) { throw new Exception('could not create the directory ' . $this->getDir() . ' for data object'); } } if (!is_writable($this->getDir())) { throw new Exception('directory ' . $this->getDir() . ' is not writeable'); } // save data if( file_put_contents( $this->getPath(), self::convertToStorage($this->data), LOCK_EX ) === false ) { throw new Exception('Could not write data to ' . $this->getPath()); } } catch (Exception $e) { if ($counter > 5) { throw new Exception( 'write failed more than five times; last path was ' . $this->getPath(), 0, $e ); } $counter++; continue; } // successfully run break; } } private function set($key, $value) { $this->data->$key = $value; } }