1: <?php
2:
3: /*****************************************************************************************
4: * X2Engine Open Source Edition is a customer relationship management program developed by
5: * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
6: *
7: * This program is free software; you can redistribute it and/or modify it under
8: * the terms of the GNU Affero General Public License version 3 as published by the
9: * Free Software Foundation with the addition of the following permission added
10: * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11: * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
12: * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
13: *
14: * This program is distributed in the hope that it will be useful, but WITHOUT
15: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16: * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
17: * details.
18: *
19: * You should have received a copy of the GNU Affero General Public License along with
20: * this program; if not, see http://www.gnu.org/licenses or write to the Free
21: * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22: * 02110-1301 USA.
23: *
24: * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
25: * California 95067, USA. or at email address [email protected].
26: *
27: * The interactive user interfaces in modified source and object code versions
28: * of this program must display Appropriate Legal Notices, as required under
29: * Section 5 of the GNU Affero General Public License version 3.
30: *
31: * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32: * these Appropriate Legal Notices must retain the display of the "Powered by
33: * X2Engine" logo. If the display of the logo is not reasonably feasible for
34: * technical reasons, the Appropriate Legal Notices must display the words
35: * "Powered by X2Engine".
36: *****************************************************************************************/
37:
38: Yii::import('application.components.EncryptedFieldsBehavior');
39: Yii::import('application.models.embedded.*');
40:
41: /**
42: * Behavior class for more advanced JSON storage in fields, using CModel children
43: * in protected/models/embedded for validation, input widget rendering, etc.
44: *
45: * Supports multiple distinct stored structures of JSON (and also, with
46: * encryption), distinguished by a separate attribute in the model (specified by
47: * {@link $templateAttr}), and each field embedded within the JSON has its own
48: * special options (i.e. default values, specific input widgets, etc) defined
49: * in the model classes used.
50: *
51: * @package application.components
52: * @author Demitri Morgan <[email protected]>
53: */
54: class JSONEmbeddedModelFieldsBehavior extends EncryptedFieldsBehavior {
55:
56: /**
57: * In this case, the structure of the embedded object will be defined in the
58: * model classes, so there's no need to define the fields in the declaration
59: * of {@link transformAttr}.
60: * @var bool
61: */
62: protected $hasOptions = false;
63:
64: /**
65: * An array storing attribute models. Eliminates the need to re-instantiate
66: * during unpacking.
67: * @var type
68: */
69: public $attrModels = array();
70:
71: public $checkObject = false;
72:
73: /**
74: * Attribute of the model class indicating whether the attribute(s) is/are encrypted.
75: * if set to false, encryption will be completely ignored.
76: * @var type
77: */
78: public $encryptedFlagAttr = false;
79:
80: /**
81: * An array of field name keys to model name values defining fields which
82: * will always use the same embedded model type.
83: * @var array
84: */
85: public $fixedModelFields = array();
86:
87: /**
88: * Specifies the name(s) of the attribute(s) of the model implementing this
89: * behavior to be used for determining the model class corresponding to the
90: * embedded model.
91: *
92: * More than one model class can be specified by declaring this an array
93: * with model field names and names of field that specify the embedded model
94: * in those fields as the values, in which case every attribute declared in
95: * {@link transformAttributes} must be declared in the array.
96: * @var string|array
97: */
98: public $templateAttr;
99:
100: /**
101: * Before attaching, check whether checking for a proper encryption object
102: * and throwing an exception if there isn't one is actually necessary.
103: * @param type $owner
104: */
105: public function attach($owner){
106: if(self::$encrypt) {
107: $this->checkObject = true;
108: }
109: parent::attach($owner);
110: }
111:
112: /**
113: * Returns the model object for the named attribute
114: *
115: * Instantiates a new model for the field and saves it in a "cache" of
116: * embedded models for the active record object if necessary.
117: * @param type $name The name of the attribute.
118: * @return JSONEmbeddedModel
119: */
120: public function attributeModel($name,$attributes=null){
121: if(!(array_key_exists($name, $this->attrModels) &&
122: ($this->getOwner()->$name instanceof JSONEmbeddedModel))){
123:
124: $owner = $this->getOwner();
125: // Resolve embedded model's class for this attribute.
126: if(array_key_exists($name, $this->fixedModelFields)){
127: // Assume a predefined, hard-coded model class to use.
128: $embeddedModelClass = $this->fixedModelFields[$name];
129: }else{ // Get the model class from another attribute.
130: if(is_array($this->templateAttr)) {
131: // There are distinct definitions for different fields each
132: // stored in a different database column
133: if(array_key_exists($name,$this->templateAttr)) {
134: $templateAttr = $this->templateAttr[$name];
135: } else {
136: throw new CException(
137: Yii::t('app','No field for {class} specifying the embedded model '.
138: 'class of its attribute {attribute} has been specified in the '.
139: 'configuration of JSONEmbeddedModelFieldsBehavior.',
140: array('{class}'=>get_class($owner),'{attribute}'=>$name)));
141: }
142: } else {
143: // There is one attribute that specifies the model class for all fields
144: // containing embedded models:
145: $templateAttr = $this->templateAttr;
146: }
147:
148: $embeddedModelClass = $owner->$templateAttr;
149: }
150:
151: if(array_key_exists($name,$this->attrModels)) {
152: // Fetch existing model
153: $embeddedModel = $this->attrModels[$name];
154: } else {
155: // Create a new model
156:
157: $embeddedModel = new $embeddedModelClass;
158: $embeddedModel->exoAttr = $name;
159: $embeddedModel->exoModel = $owner;
160: // Copy the reference into the "cache" array of models:
161: $this->attrModels[$name] = $embeddedModel;
162: }
163: if(is_array($attributes)) {
164: // Set attributes of the new model to those specified:
165: $embeddedModel->attributes = $attributes;
166: } else if(is_array($owner->$name)) {
167: // Set attributes of the new model to those existing already and stored in the
168: // model:
169: $embeddedModel->attributes = $owner->$name;
170: }
171: return $embeddedModel;
172: } else
173: return $this->getOwner()->$name;
174: }
175:
176: /**
177: * Performs validation on the embedded models, and instantiates/sets attributes
178: * of the embedded model if necessary.
179: */
180: public function beforeValidate($event) {
181: $owner = $this->getOwner();
182: foreach($this->transformAttributes as $name) {
183: $embeddedModel = $this->instantiateField($name);
184: $embeddedModel->validate();
185: if($embeddedModel->hasErrors()) {
186: $owner->addError(
187: $name,
188: Yii::t(
189: 'app','Errors encountered in {attribute}',
190: array('{attribute}'=>$owner->getAttributeLabel($name))
191: ));
192: }
193: }
194: parent::beforeValidate($event);
195: }
196:
197: /**
198: * Sets the encryption flag such that it accurately reflects the status of
199: * data going into the database.
200: * @param type $event
201: */
202: public function beforeSave($event){
203: $encryptFlag = $this->encryptedFlagAttr;
204: if((bool)$encryptFlag)
205: $this->getOwner()->$encryptFlag = self::$encrypt;
206: parent::beforeSave($event);
207: }
208:
209: /**
210: * Loads the embedded model into the owner's attribute and returns it
211: * @param string $name Attribute corresponding to the model
212: */
213: public function instantiateField($name) {
214: $owner = $this->getOwner();
215: $embeddedModel = $this->attributeModel($name);
216: if(!$owner->$name instanceof JSONEmbeddedModel)
217: $owner->$name = $embeddedModel;
218: return $embeddedModel;
219: }
220:
221: /**
222: * Instantiates all fields. This method must be called if the active record
223: * model is new.
224: */
225: public function instantiateFields(){
226: foreach($this->transformAttributes as $name) {
227: $this->instantiateField($name);
228: }
229: }
230:
231: /**
232: * JSON-encodes (and optionally encrypts) the model's attributes for storage.
233: * @param type $name
234: * @return type
235: */
236: public function packAttribute($name) {
237: $encoded = CJSON::encode($this->attributeModel($name)->attributes);
238: return self::$encrypt && (bool)
239: $this->encryptedFlagAttr ? parent::$encryption->encrypt($encoded) : $encoded;
240: }
241:
242: /**
243: * Restores the model. It will also instantiate the embedded model if it
244: * hasn't already been instantiated and "cache" it in {@link attrModels}.
245: * @param string $name
246: * @param bool $new Instantiates and returns a new model rather than using existing data
247: * @return JSONEmbeddedModel
248: */
249: public function unpackAttribute($name,$new=false) {
250: // First, fetch and decode the existing value
251: $owner = $this->getOwner();
252: $encryptedFlagAttr = $this->encryptedFlagAttr;
253: if($encryptedFlagAttr && self::$encrypt) {
254: $attributes = CJSON::decode(
255: $owner->$encryptedFlagAttr ?
256: parent::$encryption->decrypt($owner->$name) : $owner->$name);
257: } else {
258: $attributes = CJSON::decode($owner->$name);
259: }
260: // Now the values can be loaded into the model:
261: return $this->attributeModel($name,$attributes);
262: }
263:
264: }
265:
266: ?>
267: