Overview

Packages

  • application
    • commands
    • components
      • actions
      • filters
      • leftWidget
      • permissions
      • sortableWidget
      • util
      • webupdater
      • x2flow
        • actions
        • triggers
      • X2GridView
      • X2Settings
    • controllers
    • models
      • embedded
    • modules
      • accounts
        • controllers
        • models
      • actions
        • controllers
        • models
      • calendar
        • controllers
        • models
      • charts
        • models
      • contacts
        • controllers
        • models
      • docs
        • components
        • controllers
        • models
      • groups
        • controllers
        • models
      • marketing
        • components
        • controllers
        • models
      • media
        • controllers
        • models
      • mobile
        • components
      • opportunities
        • controllers
        • models
      • products
        • controllers
        • models
      • quotes
        • controllers
        • models
      • services
        • controllers
        • models
      • template
        • models
      • users
        • controllers
        • models
      • workflow
        • controllers
        • models
      • x2Leads
        • controllers
        • models
  • None
  • system
    • base
    • caching
    • console
    • db
      • ar
      • schema
    • validators
    • web
      • actions
      • auth
      • helpers
      • widgets
        • captcha
        • pagers
  • zii
    • widgets
      • grid

Classes

  • ActionFormModelBase
  • CalendarEventFormModel
  • CallFormModel
  • CreatePageFormModel
  • EditMobileFormsFormModel
  • EventCommentPublisherFormModel
  • EventFormModel
  • EventPublisherFormModel
  • FileSystemObjectDataProvider
  • MassActionFormModel
  • MobilePagination
  • NoteFormModel
  • NotificationsController
  • TimeFormModel
  • UploadLogoFormModel
  • X2FormModel
  • X2HttpRequest
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /*****************************************************************************************
  3:  * X2Engine Open Source Edition is a customer relationship management program developed by
  4:  * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
  5:  * 
  6:  * This program is free software; you can redistribute it and/or modify it under
  7:  * the terms of the GNU Affero General Public License version 3 as published by the
  8:  * Free Software Foundation with the addition of the following permission added
  9:  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
 10:  * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
 11:  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 12:  * 
 13:  * This program is distributed in the hope that it will be useful, but WITHOUT
 14:  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 15:  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
 16:  * details.
 17:  * 
 18:  * You should have received a copy of the GNU Affero General Public License along with
 19:  * this program; if not, see http://www.gnu.org/licenses or write to the Free
 20:  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 21:  * 02110-1301 USA.
 22:  * 
 23:  * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
 24:  * California 95067, USA. or at email address contact@x2engine.com.
 25:  * 
 26:  * The interactive user interfaces in modified source and object code versions
 27:  * of this program must display Appropriate Legal Notices, as required under
 28:  * Section 5 of the GNU Affero General Public License version 3.
 29:  * 
 30:  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
 31:  * these Appropriate Legal Notices must retain the display of the "Powered by
 32:  * X2Engine" logo. If the display of the logo is not reasonably feasible for
 33:  * technical reasons, the Appropriate Legal Notices must display the words
 34:  * "Powered by X2Engine".
 35:  *****************************************************************************************/
 36: 
 37: Yii::import('application.components.X2GridView.massActions.*');
 38: 
 39: abstract class MassAction extends CComponent {
 40: 
 41:     const SESSION_KEY_PREFIX = 'superMassAction';
 42:     const SESSION_KEY_PREFIX_PASS_CONFIRM = 'superMassActionPassConfirm';
 43:     const BAD_CHECKSUM = 1;
 44:     const BAD_ITEM_COUNT = 2;
 45:     const BAD_COUNT_AND_CHECKSUM = 3;
 46: 
 47:     protected static $responseForm = '';
 48: 
 49:     /**
 50:      * @var bool $hasButton If true, mass action has a button, otherwise it is assumed that the
 51:      *  mass action can only be accessed from the dropdown list
 52:      */
 53:     public $hasButton = false; 
 54: 
 55:     /**
 56:      * @var bool $allowMultiple whether or not mass action should be allowed for multiple records
 57:      */
 58:     public $allowMultiple = true; 
 59: 
 60:     /**
 61:      * @var X2GridViewBase|null $owner
 62:      */
 63:     public $owner = null; 
 64: 
 65:     /**
 66:      * If true, user must enter their password before super mass action can proceed 
 67:      */
 68:     protected $requiresPasswordConfirmation = false;
 69: 
 70:     protected $_label;
 71: 
 72:     private $_packages;
 73: 
 74:     /**
 75:      * @return string label to display in the dropdown list
 76:      */
 77:     abstract public function getLabel ();
 78: 
 79:     /**
 80:      * @param array $gvSelection array of ids of records to perform mass action on
 81:      */
 82:     abstract public function execute (array $gvSelection);
 83: 
 84:     public function renderDialog ($gridId, $modelName) {}
 85: 
 86:     public function beforeExecute () {
 87:         if ($this->getFormModel () && !$this->getFormModel ()->validate ()) {
 88:             $that = $this;
 89:             self::$responseForm = X2Widget::ajaxRender (function () use ($that) {
 90:                 $that->renderForm (false);
 91:             }, true);
 92:             return false;
 93:         }
 94:         return true;
 95:     }
 96: 
 97:     /**
 98:      * Instantiates mass action classes
 99:      * @return array  
100:      */
101:     public static function getMassActionObjects (array $classNames, X2GridViewBase $owner) {
102:         $objs = array ();
103:         foreach ($classNames as $className) {
104:             $obj = new $className;
105:             $obj->owner = $owner;
106:             $objs[] = $obj; 
107:         }
108:         return $objs;
109:     }
110: 
111:     private $_formModel;
112:     public function getFormModel () {
113:         $formModelName = get_called_class ().'FormModel';
114:         if (!in_array ($formModelName, array (
115:                 'MassAddRelationshipFormModel', 
116:                 'MassConvertRecordFormModel'
117:             )) ||
118:             !class_exists ($formModelName))  {
119: 
120:             return null;
121:         }
122:         if (!isset ($this->_formModel)) {
123:             $this->_formModel = new $formModelName;
124:             $this->_formModel->massAction = $this;
125:             if (isset ($_POST[$formModelName])) {
126:                 $this->_formModel->setAttributes ($_POST[$formModelName]);
127:             }
128:         }
129:         return $this->_formModel;
130:     }
131: 
132:     public function getModelClass () {
133:         return $this->owner ? $this->owner->modelName : Yii::app()->controller->modelClass;
134:     }
135: 
136:     public function getModelDisplayName ($plural=true) {
137:         $modelClass = $this->getModelClass ();
138:         return $modelClass::model ()->getDisplayName ($plural);
139:     }
140: 
141:     public function registerPackages () {
142:         Yii::app()->clientScript->registerPackages ($this->getPackages (), true);
143:     }
144: 
145:     public function getJSClassParams () {
146:         return array (
147:             'massActionName' => get_class ($this),
148:             'allowMultiple' => $this->allowMultiple,
149:         );
150:     }
151: 
152:     public function getPackages () {
153:         if (!isset ($this->_packages)) {
154:             $this->_packages = array (
155:                 'X2MassAction' => array(
156:                     'baseUrl' => Yii::app()->request->baseUrl,
157:                     'js' => array(
158:                         'js/X2GridView/MassAction.js',
159:                     ),
160:                     'depends' => array ('auxlib'),
161:                 ),
162:             );
163:         }
164:         return $this->_packages;
165:     }
166: 
167:     /**
168:      * Echoes flashes in the flashes arrays
169:      */
170:     public static function echoResponse () {
171:         echo CJSON::encode (static::getResponse ());
172:     }
173: 
174:     // used to hold success, warning, and error messages
175:     protected static $successFlashes = array ();
176:     protected static $noticeFlashes = array ();
177:     protected static $errorFlashes = array ();
178: 
179:     protected static function getResponse () {
180:         // encode flashes unless encode property is set to false
181:         foreach (array ('notice', 'success', 'error') as $flashType) {
182:             $prop = $flashType.'Flashes';
183:             foreach (self::$$prop as &$flash) {
184:                 if (is_array ($flash)) { 
185:                     if (!$flash['encode']) {
186:                         $flash = $flash['message'];
187:                     } else {
188:                         $flash = CHtml::encode ($flash['message']);
189:                     }
190:                 } else {
191:                     $flash = CHtml::encode ($flash);
192:                 }
193:             }
194:         }
195:         return array (
196:             'form' => self::$responseForm,
197:             'notice' => self::$noticeFlashes,
198:             'success' => self::$successFlashes,
199:             'error' => self::$errorFlashes
200:         );
201:     }
202: 
203:     /**
204:      * @param string $gridId id of grid view
205:      */
206:     public function getDialogId ($gridId) {
207:         return "$gridId-".get_class ($this)."-dialog'" ;
208:     }
209: 
210:     /**
211:      * Renders the mass action button, if applicable
212:      */
213:     public function renderButton () {
214:         if (!$this->hasButton) return;
215:         
216:         echo "
217:             <a href='#' title='".CHtml::encode ($this->getLabel ())."'
218:              data-mass-action='".get_class ($this)."'
219:              data-allow-multiple='".($this->allowMultiple ? 'true' : 'false')."'
220:              class='mass-action-button x2-button mass-action-button-".get_class ($this)."'>
221:                 <span></span>
222:             </a>";
223:     }
224: 
225:     /**
226:      * Renders the list item for the mass action dropdown 
227:      */
228:     public function renderListItem () {
229:         echo "
230:             <li class='mass-action-button mass-action-".get_class ($this)."'
231:              data-mass-action='".get_class ($this)."'
232:              data-allow-multiple='".($this->allowMultiple ? 'true' : 'false')."'".
233:             ($this->hasButton ? ' style="display: none;"' : '').">
234:             ".CHtml::encode ($this->getLabel ())."
235:             </li>";
236:     }
237: 
238:     /**
239:      * Check user password and echo either an error message or a unique id which gets used on
240:      * subsequent requests to ensure that the user confirmed the action with their password
241:      */
242:     public static function superMassActionPasswordConfirmation () {
243:         if (!isset ($_POST['password'])) 
244:             throw new CHttpException (400, Yii::t('app', 'Bad Request'));
245:         $loginForm = new LoginForm;
246:         $loginForm->username = Yii::app()->params->profile->username;
247:         $loginForm->password = $_POST['password'];
248:         if ($loginForm->validate ()) {
249:             do {
250:                 $uid = EncryptUtil::secureUniqueIdHash64 ();
251:             } while (isset ($_SESSION[self::SESSION_KEY_PREFIX_PASS_CONFIRM.$uid]));
252:             $_SESSION[self::SESSION_KEY_PREFIX_PASS_CONFIRM.$uid] = true;
253:             echo CJSON::encode (array (true, $uid));
254:         } else {
255:             echo CJSON::encode (array (false, Yii::t('app', 'incorrect password')));
256:         }
257:     }
258: 
259:     protected function renderForm () {}
260: 
261:     /**
262:      * @return bool true if attribute is a valid filter or sort attribute for given model, false 
263:      *  otherwise
264:      */
265:     protected function isValidAttribute ($className, $attr) {
266:         $staticModel = X2Model::model ($className);
267:         return 
268:             ($staticModel->hasAttribute ($attr) || 
269:              $attr === 'tags' && $staticModel->asa ('TagBehavior'));
270:     }
271: 
272:     /**
273:      * Helper method for superExecute. Returns array of ids of records in search results.
274:      * @param string $modelClass
275:      * @return array array of ids and their checksum
276:      */
277:     protected function getIdsFromSearchResults ($modelClass) {
278:         // copy sort & filter parameters from POST data to GET superglobal so
279:         // that ERememberFiltersBehavior and SmartDataProviderBehavior will filter/sort records
280:         // properly
281:         if (isset ($_POST[$modelClass])) {
282:             $_GET[$modelClass] = $_POST[$modelClass];
283: 
284:             // ensure that specified filter attributes are valid
285:             foreach ($_GET[$modelClass] as $attr => $val) {
286: 
287:                 if (!$this->isValidAttribute ($modelClass, $attr)) {
288:                     throw new CHttpException (400, Yii::t('app', 'Bad Request'));
289:                 }
290:             }
291:         }
292: 
293:         if (isset ($_POST[$modelClass.'_sort'])) {
294:             $_GET[$modelClass.'_sort'] = $_POST[$modelClass.'_sort'];
295: 
296:             // ensure that specified sort order attribute is valid
297:             $sortAttr = preg_replace ('/\.desc$/', '', $_GET[$modelClass.'_sort']);
298: 
299:             if (!$this->isValidAttribute ($modelClass, $sortAttr)) {
300:                 throw new CHttpException (400, Yii::t('app', 'Bad Request'));
301:             }
302:         }
303: 
304:         // data provider is retrieved for the sole purpose of using it's criteria object
305:         $model = new $modelClass ('search', null, false, true);
306:         $dataProvider = $model->search (0); // page size set to 0 to improve performance
307:         $dataProvider->calculateChecksum = true;
308:         $dataProvider->getData (); // force checksum to be calculated
309:         $ids = $dataProvider->getRecordIds ();
310:         //AuxLib::debugLogR ($ids);
311:         $idChecksum = $dataProvider->getidChecksum ();
312: 
313:         // reverse sort order so that we can pop from id list instead of pushing
314:         $ids = array_reverse ($ids);
315: 
316:         return array ($ids, $idChecksum);
317:     }
318: 
319:     /**
320:      * Execute mass action on next batch of records
321:      * @param string $uid unique id
322:      * @param int $totalItemCount total number of records to operate on
323:      * @param string $expectedIdChecksum checksum of ids of records in data provider used to 
324:      *  generate the grid view
325:      */
326:     public function superExecute ($uid, $totalItemCount, $expectedIdChecksum) {
327:         //$timer = new TimerUtil;
328:         //$timer->start ();
329:         // clear saved ids if user clicked the stop button
330:         if (isset ($_POST['clearSavedIds']) && $_POST['clearSavedIds']) {
331:             if (!empty ($uid)) {
332:                 unset ($_SESSION[self::SESSION_KEY_PREFIX.$uid]);
333:                 unset ($_SESSION[self::SESSION_KEY_PREFIX_PASS_CONFIRM.$uid]);
334:             }
335:             echo 'success';
336:             return;
337:         }
338: 
339:         // ensure that for super mass deletion, user confirmed deletion with password
340:         if ($this->requiresPasswordConfirmation && (empty ($uid) || 
341:             !isset ($_SESSION[self::SESSION_KEY_PREFIX_PASS_CONFIRM.$uid]) ||
342:             !$_SESSION[self::SESSION_KEY_PREFIX_PASS_CONFIRM.$uid])) {
343: 
344:             throw new CHttpException (
345:                 401, Yii::t('app', 'You are not authorized to perform this action'));
346:         }
347:         if (!$this->requiresPasswordConfirmation && !empty ($uid) && 
348:             !isset ($_SESSION[self::SESSION_KEY_PREFIX.$uid])) { 
349: 
350:             /**/AuxLib::debugLogR ('Error: $uid is not empty and SESSION key is not set');
351:             throw new CHttpException (400, Yii::t('app', 'Bad Request'));
352:         }
353: 
354:         $modelClass = Yii::app()->controller->modelClass;
355: 
356:         //$timer->stop ()->read ('first')->reset ()->start ();
357: 
358:         // if super mass operation hasn't started, initialize id list from which batches will
359:         // be retrieved
360:         if (empty ($uid) ||
361:             (!isset ($_SESSION[self::SESSION_KEY_PREFIX.$uid]) &&
362:              $this->requiresPasswordConfirmation)) {
363: 
364:             if (!$this->requiresPasswordConfirmation) {
365:                 // removes the even the remote possibility of a key collision
366:                 do {
367:                     $uid = uniqid (false, true);
368:                 } while (isset ($_SESSION[self::SESSION_KEY_PREFIX.$uid]));
369:             }
370:             list ($ids, $idChecksum) = $this->getIdsFromSearchResults ($modelClass);
371: 
372:             // This important check ensures that the number of records displayed in the grid view
373:             // is equal to the number of records filtered by the specified filters. This check
374:             // greatly reduces that chance of an incorrect update/deletion.
375:             if (count ($ids) !== $totalItemCount || $idChecksum !== $expectedIdChecksum) {
376:                 if (count ($ids) !== $totalItemCount && $idChecksum !== $expectedIdChecksum) {
377:                     $errorCode = self::BAD_COUNT_AND_CHECKSUM;
378:                 } else if (count ($ids) !== $totalItemCount) {
379:                     $errorCode = self::BAD_ITEM_COUNT;
380:                 } else {
381:                     $errorCode = self::BAD_CHECKSUM;
382:                 }
383:                 echo CJSON::encode (array (
384:                     'failure' => true, 
385:                     'errorMessage' => Yii::t('app', 
386:                         'The data being displayed in this grid view is out of date. Close '.
387:                         'this dialog and allow the grid to refresh before attempting this '.
388:                         'mass action again.'),
389:                     'errorCode' => $errorCode,
390:                 ));
391:                 return;
392:             }
393:             $_SESSION[self::SESSION_KEY_PREFIX.$uid] = $ids;
394:         }
395: 
396:         //$timer->stop ()->read ('second')->reset ()->start ();
397: 
398:         // grab next batch of ids from session
399:         $selectedRecords = $_SESSION[self::SESSION_KEY_PREFIX.$uid];
400:         $selectedRecordsCount = count ($selectedRecords);
401:         $batchSize = Yii::app()->settings->massActionsBatchSize;
402:         $batchSize = $selectedRecordsCount < $batchSize ? $selectedRecordsCount : $batchSize;
403:         $batch = array ();
404:         for ($i = 0; $i < $batchSize; $i++) {
405:             // for efficiency reasons, record ids are stored in reverse order and popped.
406:             // array_shift = O(n), array_pop = O(1)
407:             $batch[] = array_pop ($selectedRecords);
408:         }
409:         $_SESSION[self::SESSION_KEY_PREFIX.$uid] = $selectedRecords;
410: 
411:         // execute mass action on batch
412:         $successes = $this->execute ($batch);
413: 
414:         // clear session once all batches have been completed
415:         if (count ($selectedRecords) === 0) {
416:             unset ($_SESSION[self::SESSION_KEY_PREFIX.$uid]);
417:             unset ($_SESSION[self::SESSION_KEY_PREFIX_PASS_CONFIRM.$uid]);
418:         }
419: 
420:         $response = $this->generateSuperMassActionResponse ($successes, $selectedRecords, $uid);
421: 
422:         //$timer->stop ()->read ('third')->reset ()->start ();
423: 
424:         echo CJSON::encode ($response);
425:     }
426: 
427:     /**
428:      * @param int $successes number of records for which mass action was successful
429:      * @param array records yet to be acted upon
430:      * @return array response data for super mass action request 
431:      */
432:     protected function generateSuperMassActionResponse ($successes, $selectedRecords, $uid) {
433:         $flashes = self::getResponse ();
434:         $response = $flashes;
435:         $response['successes'] = $successes;
436:         $response['uid'] = $uid;
437:         if (count ($selectedRecords) === 0) {
438:             $response['complete'] = true;
439:         } else {
440:             $response['batchComplete'] = true;
441:         }
442:         return $response;
443:     }
444: 
445: }
446: 
447: abstract class MassActionFormModel extends CFormModel {
448:     public $massAction = null;
449: }
450: 
X2CRM Documentation API documentation generated by ApiGen 2.8.0