1<?php 2/* 3 * e107 website system 4 * 5 * Copyright (C) 2008-2010 e107 Inc (e107.org) 6 * Released under the terms and conditions of the 7 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt) 8 * 9 * e107 Base Model 10 * 11 * $Id$ 12 * $Author$ 13*/ 14 15if (!defined('e107_INIT')) { exit; } 16 17/** 18 * Base e107 Object class 19 * 20 * @package e107 21 * @category e107_handlers 22 * @version 1.0 23 * @author SecretR 24 * @copyright Copyright (C) 2010, e107 Inc. 25 */ 26class e_object 27{ 28 /** 29 * Object data 30 * 31 * @var array 32 */ 33 protected $_data = array(); 34 35 /** 36 * Model parameters passed mostly from external sources 37 * 38 * @var array 39 */ 40 protected $_params = array(); 41 42 43 /** 44 * Name of object id field 45 * Required for {@link getId()()} method 46 * 47 * @var string 48 */ 49 protected $_field_id; 50 51 /** 52 * Constructor - set data on initialization 53 * 54 * @param array $data 55 */ 56 function __construct($data = array()) 57 { 58 if(is_array($data)) $this->setData($data); 59 } 60 61 /** 62 * Set name of object's field id 63 * 64 * @see getId() 65 * 66 * @param string $name 67 * @return object e_object 68 */ 69 public function setFieldIdName($name) 70 { 71 $this->_field_id = $name; 72 return $this; 73 } 74 75 /** 76 * Retrieve name of object's field id 77 * 78 * @see getId() 79 * 80 * @param string $name 81 * @return string 82 */ 83 public function getFieldIdName() 84 { 85 return $this->_field_id; 86 } 87 88 /** 89 * Retrieve object primary id field value 90 * 91 * @return mixed 92 */ 93 public function getId() 94 { 95 96 if ($this->getFieldIdName()) 97 { 98 return $this->get($this->getFieldIdName(), 0); // default of NULL will break MySQL strict in most cases. 99 } 100 return $this->get('id', 0); 101 } 102 103 /** 104 * Set object primary id field value 105 * 106 * @return e_object 107 */ 108 public function setId($id) 109 { 110 if ($this->getFieldIdName()) 111 { 112 return $this->set($this->getFieldIdName(), $id); 113 } 114 return $this; 115 } 116 117 /** 118 * Retrieves data from the object ($_data) without 119 * key parsing (performance wise, prefered when possible) 120 * 121 * @param string $key 122 * @param mixed $default 123 * @return mixed 124 */ 125 public function get($key, $default = null) 126 { 127 return (isset($this->_data[$key]) ? $this->_data[$key] : $default); 128 } 129 130 /** 131 * Get object data 132 * @return array 133 */ 134 public function getData() 135 { 136 return $this->_data; 137 } 138 139 /** 140 * Overwrite data in the object for a single field. 141 * 142 * @param string $key 143 * @param mixed $value 144 * @return e_object 145 */ 146 public function set($key, $value) 147 { 148 $this->_data[$key] = $value; 149 return $this; 150 } 151 152 /** 153 * Set object data 154 * @return e_object 155 */ 156 public function setData($data) 157 { 158 $this->_data = $data; 159 return $this; 160 } 161 162 /** 163 * Update object data 164 * @return e_object 165 */ 166 public function addData($data) 167 { 168 foreach($data as $key => $val) 169 { 170 $this->set($key, $val); 171 } 172 return $this; 173 } 174 175 /** 176 * Remove object data key 177 * 178 * @param string $key 179 * @return e_object 180 */ 181 public function remove($key) 182 { 183 unset($this->_data[$key]); 184 return $this; 185 } 186 187 /** 188 * Reset object data key 189 * 190 * @return e_object 191 */ 192 public function removeData() 193 { 194 $this->_data = array(); 195 return $this; 196 } 197 198 /** 199 * Check if key is set 200 * @param string $key 201 * @return boolean 202 */ 203 public function is($key) 204 { 205 return (isset($this->_data[$key])); 206 } 207 208 /** 209 * Check if key is set and not empty 210 * @param string $key 211 * @return boolean 212 */ 213 public function has($key) 214 { 215 return (isset($this->_data[$key]) && !empty($this->_data[$key])); 216 } 217 218 /** 219 * Check if object has data 220 * @return boolean 221 */ 222 public function hasData() 223 { 224 return !empty($this->_data); 225 } 226 227 /** 228 * Set parameter array 229 * @param array $params 230 * @return e_object 231 */ 232 public function setParams(array $params) 233 { 234 $this->_params = $params; 235 return $this; 236 } 237 238 /** 239 * Update parameter array 240 * @param array $params 241 * @return object e_object 242 */ 243 public function updateParams(array $params) 244 { 245 foreach ($params as $k => $v) 246 { 247 $this->setParam($k, $v); 248 } 249 250 return $this; 251 } 252 253 /** 254 * Get parameter array 255 * 256 * @return array parameters 257 */ 258 public function getParams() 259 { 260 return $this->_params; 261 } 262 263 /** 264 * Set parameter 265 * 266 * @param string $key 267 * @param mixed $value 268 * @return e_tree_model 269 */ 270 public function setParam($key, $value) 271 { 272 if(null === $value) 273 { 274 unset($this->_params[$key]); 275 } 276 else $this->_params[$key] = $value; 277 278 return $this; 279 } 280 281 /** 282 * Get parameter 283 * 284 * @param string $key 285 * @param mixed $default 286 */ 287 public function getParam($key, $default = null) 288 { 289 return (isset($this->_params[$key]) ? $this->_params[$key] : $default); 290 } 291 292 /** 293 * Convert object data to simple shortcodes (e_vars object) 294 * @return string 295 */ 296 public function toSc() 297 { 298 return new e_vars($this->_data); 299 } 300 301 /** 302 * Convert object data to array 303 * @return string 304 */ 305 public function toJson() 306 { 307 return json_encode($this->_data); 308 } 309 310 /** 311 * Convert object to array 312 * @return array object data 313 */ 314 public function toArray() 315 { 316 return $this->_data; 317 } 318 319 /** 320 * Magic method - convert object data to an array 321 * 322 * @return array 323 */ 324 public function __toArray() 325 { 326 return $this->toArray(); 327 } 328 329 /** 330 * Convert object data to a string 331 * 332 * @param boolean $AddSlashes 333 * @return string 334 */ 335 public function toString($AddSlashes = false) 336 { 337 return (string) e107::getArrayStorage()->WriteArray($this->toArray(), $AddSlashes); 338 } 339 340 /** 341 * Magic method - convert object data to a string 342 * NOTE: before PHP 5.2.0 the __toString method was only 343 * called when it was directly combined with echo() or print() 344 * 345 * NOTE: PHP 5.3+ is throwing parse error if __toString has optional arguments. 346 * 347 * @return string 348 */ 349 public function __toString() 350 { 351 return $this->toString(false); 352 } 353 354 /** 355 * Magic setter 356 * Triggered on e.g. <code><?php $e_object->myKey = 'someValue'; </code> 357 * 358 * @param string $key 359 * @param mixed $value 360 */ 361 public function __set($key, $value) 362 { 363 // Unset workaround - PHP < 5.1.0 364 if(null === $value) $this->remove($key); 365 else $this->set($key, $value); 366 } 367 368 /** 369 * Magic getter 370 * Triggered on e.g. <code><?php print($e_object->myKey); </code> 371 * @param string $key 372 * @return mixed value or null if key not found 373 */ 374 public function __get($key) 375 { 376 if($this->is($key)) 377 { 378 return $this->get($key); 379 } 380 381 return null; 382 } 383 384 /** 385 * Magic method to check if given data key is set. 386 * Triggered on <code><?php isset($e_object->myKey); </code> 387 * NOTE: works on PHP 5.1.0+ 388 * 389 * @param string $key 390 * @return boolean 391 */ 392 public function __isset($key) 393 { 394 return $this->is($key); 395 } 396 397 /** 398 * Magic method to unset given data key. 399 * Triggered on <code><?php unset($e_object->myKey); </code> 400 * NOTE: works on PHP 5.1.0+ 401 * 402 * @param string $key 403 */ 404 public function __unset($key) 405 { 406 $this->remove($key); 407 } 408} 409 410 411/** 412 * Data object for e_parse::simpleParse() 413 * NEW - not inherits core e_object 414 * Moved from e_parse_class.php 415 * Could go in separate file in the future, together with e_object class 416 */ 417class e_vars extends e_object 418{ 419 /** 420 * Get data array 421 * 422 * @return array 423 */ 424 public function getVars() 425 { 426 return $this->getData(); 427 } 428 429 /** 430 * Set array data 431 * 432 * @param array $array 433 * @return e_vars 434 */ 435 public function setVars(array $array) 436 { 437 $this->setData($array); 438 return $this; 439 } 440 441 /** 442 * Add array data to the object (merge with existing) 443 * 444 * @param array $array 445 * @return e_vars 446 */ 447 public function addVars(array $array) 448 { 449 $this->addData($array); 450 } 451 452 /** 453 * Reset object data 454 * 455 * @return e_vars 456 */ 457 public function emptyVars() 458 { 459 $this->removeData(); 460 return $this; 461 } 462 463 /** 464 * Check if there is data available 465 * 466 * @return boolean 467 */ 468 public function isEmpty() 469 { 470 return (!$this->hasData()); 471 } 472 473 /** 474 * Check if given data key is set 475 * @param string $key 476 * @return boolean 477 */ 478 public function isVar($key) 479 { 480 return $this->is($key); 481 } 482 483 /** 484 * No need of object conversion, optional cloning 485 * @param boolean $clone return current object clone 486 * @return e_vars 487 */ 488 public function toSc($clone = false) 489 { 490 if($clone) return clone $this; 491 return $this; 492 } 493} 494 495/** 496 * Base e107 Model class 497 * 498 * @package e107 499 * @category e107_handlers 500 * @version 1.0 501 * @author SecretR 502 * @copyright Copyright (C) 2010, e107 Inc. 503 */ 504class e_model extends e_object 505{ 506 /** 507 * Data structure (types) array, required for {@link e_front_model::sanitize()} method, 508 * it also serves as a map (find data) for building DB queries, 509 * copy/sanitize posted data to object data, etc. 510 * 511 * This can/should be overwritten by extending the class 512 * 513 * @var array 514 */ 515 protected $_data_fields = array(); 516 517 518 519 /** 520 * Current model field types eg. text, bbarea, dropdown etc. 521 * 522 * 523 * @var string 524 */ 525 protected $_field_input_types = array(); 526 527 528 /** 529 * Current model DB table, used in all db calls 530 * 531 * This can/should be overwritten/set by extending the class 532 * 533 * @var string 534 */ 535 protected $_db_table; 536 537 /** 538 * Current url Profile data 539 * Example: array('route'=>'page/view/index', 'vars' => array('id' => 'page_id', 'sef' => 'page_sef'), 'name' => 'page_title', 'description' => ''); 540 * @var string 541 */ 542 protected $_url = array(); 543 544 545 /** 546 * Current Featurebox Profile data 547 * Example: array('title' => 'page_title', 'text' => ''); 548 * @var string 549 */ 550 protected $_featurebox = array(); 551 552 /** 553 * Runtime cache of parsed from {@link _getData()} keys 554 * 555 * @var array 556 */ 557 protected $_parsed_keys = array(); 558 559 /** 560 * Avoid DB calls if data is not changed 561 * 562 * @see _setData() 563 * @var boolean 564 */ 565 protected $data_has_changed = false; 566 567 /** 568 * Namespace to be used for model related system messages in {@link eMessage} handler 569 * 570 * @var string 571 */ 572 protected $_message_stack = 'default'; 573 574 /** 575 * Cache string to be used from _get/set/clearCacheData() methods 576 * 577 * @var string 578 */ 579 protected $_cache_string = null; 580 581 /** 582 * Force Cache even if system cahche is disabled 583 * Default is false 584 * 585 * @var boolean 586 */ 587 protected $_cache_force = false; 588 589 590 /** 591 * Optional DB table - used for auto-load data from the DB 592 * @param string $table 593 * @return e_model 594 */ 595 public function getModelTable() 596 { 597 return $this->_db_table; 598 } 599 600 /** 601 * Set model DB table 602 * @param string $table 603 * @return e_model 604 */ 605 public function setModelTable($table) 606 { 607 $this->_db_table = $table; 608 return $this; 609 } 610 611 612 /** 613 * Set model Url Profile 614 * @param string $table 615 * @return e_model 616 */ 617 public function setUrl($url) 618 { 619 if(!is_array($url)) $url = array('route' => $url); 620 $this->_url = $url; 621 return $this; 622 } 623 624 /** 625 * Get url profile 626 * @return array 627 */ 628 public function getUrl() 629 { 630 return $this->_url; 631 } 632 633 /** 634 * Set model Featurebox Profile 635 * @param string $table 636 * @return e_model 637 */ 638 public function setFeaturebox($fb) 639 { 640 // if(!is_array($url)) $url = array('route' => $url); 641 $this->_featurebox = $fb; 642 return $this; 643 } 644 645 /** 646 * Get Featurebox profile 647 * @return array 648 */ 649 public function getFeaturebox() 650 { 651 return $this->_featurebox; 652 } 653 654 655 /** 656 * Generic URL assembling method 657 * @param array $options [optional] see eRouter::assemble() for $options structure 658 * @param boolean $extended [optional] if true, method will return an array containing url, title and description of the url 659 * @return mixed URL string or extended array data 660 */ 661 public function url($ids, $options = array(), $extended = false) 662 { 663 $urldata = $this->getUrl(); 664 if(empty($urldata) || !vartrue($urldata['route'])) return ($extended ? array() : null); 665 666 $eurl = e107::getUrl(); 667 668 if(empty($options)) $options = array(); 669 elseif(!is_array($options)) parse_str($options, $options); 670 671 $vars = $this->toArray(); 672 if(!isset($options['allow']) || empty($options['allow'])) 673 { 674 if(vartrue($urldata['vars']) && is_array($urldata['vars'])) 675 { 676 $vars = array(); 677 foreach ($urldata['vars'] as $var => $field) 678 { 679 if($field === true) $field = $var; 680 $vars[$var] = $this->get($field); 681 } 682 } 683 } 684 685 $method = isset($options['sc']) ? 'sc' : 'create'; 686 687 $url = e107::getUrl()->$method($urldata['route'], $vars, $options); 688 689 if(!$extended) 690 { 691 return $url; 692 } 693 694 return array( 695 'url' => $url, 696 'name' => vartrue($urldata['name']) ? $this->get($urldata['name']) : '', 697 'description' => vartrue($urldata['description']) ? $this->get($urldata['description']) : '', 698 ); 699 } 700 701 702 /** 703 * Generic Featurebox assembling method 704 * @return mixed URL string or extended array data 705 */ 706 public function featurebox($options = array(), $extended = false) 707 { 708 709 710 } 711 712 713 714 715 716 717 718 719 720 721 722 723 724 /** 725 * Get data fields array 726 * @return array 727 */ 728 public function getDataFields() 729 { 730 return $this->_data_fields; 731 } 732 733 734 /** 735 * @param $key 736 * @return bool 737 */ 738 public function getFieldInputType($key) 739 { 740 if(isset($this->_field_input_types[$key])) 741 { 742 return $this->_field_input_types[$key]; 743 } 744 745 return false; 746 } 747 748 /** 749 * Set Predefined data fields in format key => type 750 * @return object e_model 751 */ 752 public function setDataFields($data_fields) 753 { 754 $this->_data_fields = $data_fields; 755 return $this; 756 } 757 758 /** 759 * Set Predefined data fields in format key => type 760 * @return e_model 761 */ 762 public function setFieldInputTypes($fields) 763 { 764 $this->_field_input_types = $fields; 765 return $this; 766 } 767 768 /** 769 * Set Predefined data field 770 * @return e_model 771 */ 772 public function setDataField($field, $type) 773 { 774 $this->_data_fields[$field] = $type; 775 return $this; 776 } 777 778 /** 779 * Retrieves data from the object ($_data) without 780 * key parsing (performance wise, prefered when possible) 781 * 782 * @see _getDataSimple() 783 * @param string $key 784 * @param mixed $default 785 * @return mixed 786 */ 787 public function get($key, $default = null) 788 { 789 return $this->_getDataSimple((string) $key, $default); 790 } 791 792 /** 793 * Retrieves data from the object ($_data) 794 * If $key is empty, return all object data 795 * 796 * @see _getData() 797 * @param string $key 798 * @param mixed $default 799 * @param integer $index 800 * @return mixed 801 */ 802 public function getData($key = '', $default = null, $index = null) 803 { 804 return $this->_getData($key, $default, $index); 805 } 806 807 /** 808 * Overwrite data in the object for a single field. Key is not parsed. 809 * Public proxy of {@link _setDataSimple()} 810 * Data isn't sanitized so use this method only when data comes from trustable sources (e.g. DB) 811 * 812 * 813 * @see _setData() 814 * @param string $key 815 * @param mixed $value 816 * @param boolean $strict update only 817 * @return e_model 818 */ 819 public function set($key, $value = null, $strict = false) 820 { 821 return $this->_setDataSimple($key, $value, $strict); 822 } 823 824 /** 825 * Overwrite data in the object. Public proxy of {@link _setData()} 826 * Data isn't sanitized so use this method only when data comes from trustable sources (e.g. DB) 827 * 828 * @see _setData() 829 * @param string|array $key 830 * @param mixed $value 831 * @param boolean $strict update only 832 * @return e_model 833 */ 834 public function setData($key, $value = null, $strict = false) 835 { 836 return $this->_setData($key, $value, $strict); 837 } 838 839 /** 840 * Add data to the object. 841 * Retains existing data in the object. 842 * Public proxy of {@link _addData()} 843 * 844 * If $override is false, data will be updated only (check against existing data) 845 * 846 * @param string|array $key 847 * @param mixed $value 848 * @param boolean $override override existing data 849 * @return e_model 850 */ 851 public function addData($key, $value = null, $override = true) 852 { 853 return $this->_addData($key, $value, $override); 854 } 855 856 /** 857 * Unset single field from the object. 858 * Public proxy of {@link _unsetDataSimple()} 859 * 860 * @param string $key 861 * @return e_model 862 */ 863 public function remove($key) 864 { 865 return $this->_unsetDataSimple($key); 866 } 867 868 /** 869 * Unset data from the object. 870 * $key can be a string only. Array will be ignored. 871 * '/' inside the key will be treated as array path 872 * if $key is null entire object will be reset 873 * 874 * Public proxy of {@link _unsetData()} 875 * 876 * @param string|null $key 877 * @return e_model 878 */ 879 public function removeData($key = null) 880 { 881 return $this->_unsetData($key); 882 } 883 884 /** 885 * @param string $key 886 * @return boolean 887 */ 888 public function has($key) 889 { 890 return $this->_hasData($key); 891 } 892 893 /** 894 * @param string $key 895 * @return boolean 896 */ 897 public function hasData($key = '') 898 { 899 return $this->_hasData($key); 900 } 901 902 /** 903 * @param string $key 904 * @return boolean 905 */ 906 public function isData($key) 907 { 908 return $this->_isData($key); 909 } 910 911 /** 912 * @param boolean $new_state new object state if set 913 * @return boolean 914 */ 915 public function isModified($new_state = null) 916 { 917 if(is_bool($new_state)) 918 { 919 $this->data_has_changed = $new_state; 920 } 921 return $this->data_has_changed; 922 } 923 924 /** 925 * Retrieves data from the object 926 * 927 * If $key is empty will return all the data as an array 928 * Otherwise it will return value of the attribute specified by $key 929 * '/' inside the key will be treated as array path (x/y/z equals to [x][y][z] 930 * 931 * If $index is specified it will assume that attribute data is an array 932 * and retrieve corresponding member. 933 * 934 * NEW: '/' supported in keys now, just use double slashes '//' as key separator 935 * Examples: 936 * - 'key//some/key/with/slashes//more' -> [key][some/key/with/slashes][more] 937 * - '//some/key' -> [some/key] - starting with // means - don't parse! 938 * - '///some/key/' -> [/some/key/] 939 * - '//key//some/key/with/slashes//more' WRONG -> single key [key//some/key/with/slashes//more] 940 * 941 * @param string $key 942 * @param mixed $default 943 * @param integer $index 944 * @param boolean $posted data source 945 * @return mixed 946 */ 947 protected function _getData($key = '', $default = null, $index = null, $data_src = '_data') 948 { 949 if ('' === $key) 950 { 951 return $this->$data_src; 952 } 953 954 $simple = false; 955 if(strpos($key, '//') === 0) 956 { 957 $key = substr($key, 2); 958 $simple = true; 959 } 960 /*elseif($key[0] == '/') 961 { 962 // just use it! 963 $simple = true; 964 }*/ 965 else 966 { 967 $simple = strpos($key, '/') === false; 968 } 969 970 // Fix - check if _data[path/to/value] really doesn't exist 971 if (!$simple) 972 { 973 //$key = trim($key, '/'); 974 if(isset($this->_parsed_keys[$data_src.'/'.$key])) 975 { 976 return $this->_parsed_keys[$data_src.'/'.$key]; 977 } 978 // new feature (double slash) - when searched key string is key//some/key/with/slashes//more 979 // -> search for 'key' => array('some/key/with/slashes', array('more' => value)); 980 $keyArr = explode(strpos($key, '//') ? '//' : '/', $key); 981 $data = $this->$data_src; 982 foreach ($keyArr as $i => $k) 983 { 984 if ('' === $k) 985 { 986 return $default; 987 } 988 989 if (is_array($data)) 990 { 991 if (!isset($data[$k])) 992 { 993 return $default; 994 } 995 $data = $data[$k]; 996 } 997 else 998 { 999 return $default; 1000 } 1001 } 1002 $this->_parsed_keys[$data_src.'/'.$key] = $data; 1003 return $data; 1004 } 1005 1006 //get $index 1007 if (isset($this->{$data_src}[$key])) 1008 { 1009 if (null === $index) 1010 { 1011 return $this->{$data_src}[$key]; 1012 } 1013 1014 $value = $this->{$data_src}[$key]; 1015 if (is_array($value)) 1016 { 1017 if (isset($value[$index])) 1018 { 1019 return $value[$index]; 1020 } 1021 return $default; 1022 } 1023 elseif (is_string($value)) 1024 { 1025 $arr = explode("\n", $value); 1026 return (isset($arr[$index]) ? $arr[$index] : $default); 1027 } 1028 return $default; 1029 } 1030 return $default; 1031 } 1032 1033 /** 1034 * Get value from _data array without parsing the key 1035 * 1036 * @param string $key 1037 * @param mixed $default 1038 * @param string $posted data source 1039 * @return mixed 1040 */ 1041 protected function _getDataSimple($key, $default = null, $data_src = '_data') 1042 { 1043 return isset($this->{$data_src}[$key]) ? $this->{$data_src}[$key] : $default; 1044 } 1045 1046 /** 1047 * Overwrite data in the object. 1048 * 1049 * $key can be string or array. 1050 * If $key is string, the attribute value will be overwritten by $value 1051 * '/' inside the key will be treated as array path 1052 * 1053 * If $key is an array and $strict is false, it will overwrite all the data in the object. 1054 * 1055 * If $strict is true and $data_src is '_data', data will be updated only (no new data will be added) 1056 * 1057 * NEW: '/' supported in keys now, just use double slashes '//' as key separator 1058 * Examples: 1059 * - 'key//some/key/with/slashes//more' -> [key][some/key/with/slashes][more] 1060 * - '//some/key' -> [some/key] - starting with // means - don't parse! 1061 * - '///some/key/' -> [/some/key/] 1062 * - '//key//some/key/with/slashes//more' WRONG -> single key [key//some/key/with/slashes//more] 1063 * 1064 * 1065 * @param string|array $key 1066 * @param mixed $value 1067 * @param boolean $strict 1068 * @param string $data_src 1069 * @return e_model 1070 */ 1071 protected function _setData($key, $value = null, $strict = false, $data_src = '_data') 1072 { 1073 if(is_array($key)) 1074 { 1075 if($strict) 1076 { 1077 foreach($key as $k => $v) 1078 { 1079 $this->_setData($k, $v, true, $data_src); 1080 } 1081 return $this; 1082 } 1083 1084 $this->$data_src = $key; 1085 return $this; 1086 } 1087 1088 //multidimensional array support - strict _setData for values of type array 1089 if($strict && !empty($value) && is_array($value)) 1090 { 1091 foreach($value as $k => $v) 1092 { 1093 // new - $k couldn't be a path - e.g. 'key' 'value/value1' 1094 // will result in 'key' => 'value/value1' and NOT 'key' => array('value' => value1) 1095 $this->_setData($key.'//'.$k, $v, true, $data_src); 1096 } 1097 return $this; 1098 } 1099 1100 $simple = false; 1101 if(strpos($key, '//') === 0) 1102 { 1103 // NEW - leading '//' means set 'some/key' without parsing it 1104 // Example: '//some/key'; NOTE: '//some/key//more/depth' is NOT parsed 1105 // if you wish to have array('some/key' => array('more/depth' => value)) 1106 // right syntax is 'some/key//more/depth' 1107 $key = substr($key, 2); 1108 $simple = true; 1109 } 1110 /*elseif($key[0] == '/') 1111 { 1112 $simple = true; 1113 }*/ 1114 else 1115 { 1116 $simple = strpos($key, '/') === false; 1117 } 1118 1119 //multidimensional array support - parse key 1120 if(!$simple) 1121 { 1122 //$key = trim($key, '/'); 1123 //if strict - update only 1124 if($strict && !$this->isData($key)) 1125 { 1126 return $this; 1127 } 1128 1129 // new feature (double slash) - when parsing key: key//some/key/with/slashes//more 1130 // -> result is 'key' => array('some/key/with/slashes', array('more' => value)); 1131 $keyArr = explode(strpos($key, '//') ? '//' : '/', $key); 1132 //$keyArr = explode('/', $key); 1133 $data = &$this->{$data_src}; 1134 for ($i = 0, $l = count($keyArr); $i < $l; $i++) 1135 { 1136 1137 $k = $keyArr[$i]; 1138 1139 if (!isset($data[$k]) || empty($data[$k])) // PHP7.1 fix. Reset to empty array() if $data[$k] is an empty string. Reason for empty string still unknown. 1140 { 1141 $data[$k] = array(); 1142 } 1143 1144 $data = &$data[$k]; 1145 } 1146 1147 //data has changed - optimized 1148 if('_data' === $data_src && !$this->data_has_changed) 1149 { 1150 $this->data_has_changed = (!isset($this->_data[$key]) || $this->_data[$key] != $value); 1151 } 1152 $this->_parsed_keys[$data_src.'/'.$key] = $value; 1153 $data = $value; 1154 } 1155 else 1156 { 1157 //if strict - update only 1158 if($strict && !isset($this->_data[$key])) 1159 { 1160 return $this; 1161 } 1162 if('_data' === $data_src && !$this->data_has_changed) 1163 { 1164 $this->data_has_changed = (!isset($this->_data[$key]) || $this->{$data_src}[$key] != $value); 1165 } 1166 $this->{$data_src}[$key] = $value; 1167 } 1168 1169 return $this; 1170 } 1171 1172 /** 1173 * Set data for the given source. More simple (and performance wise) version 1174 * of {@link _setData()} 1175 * 1176 * @param string $key 1177 * @param mixed $value 1178 * @param boolean $strict 1179 * @param string $data_src 1180 * @return e_model 1181 */ 1182 protected function _setDataSimple($key, $value = null, $strict = false, $data_src = '_data') 1183 { 1184 $key = $key.'';//smart toString 1185 if(!$strict) 1186 { 1187 //data has changed 1188 if('_data' === $data_src && !$this->data_has_changed) 1189 { 1190 $this->data_has_changed = (!isset($this->_data[$key]) || $this->_data[$key] != $value); 1191 } 1192 $this->{$data_src}[$key] = $value; 1193 return $this; 1194 } 1195 1196 if($this->isData($key)) 1197 { 1198 if('_data' === $data_src && !$this->data_has_changed) 1199 { 1200 $this->data_has_changed = (!isset($this->_data[$key]) || $this->_data[$key] != $value); 1201 } 1202 $this->{$data_src}[$key] = $value; 1203 } 1204 1205 return $this; 1206 } 1207 1208 /** 1209 * Add data to the object. 1210 * Retains existing data in the object. 1211 * 1212 * If $override is false, only new (non-existent) data will be added 1213 * 1214 * @param string|array $key 1215 * @param mixed $value 1216 * @param boolean $override allow override of existing data 1217 * @param string $data_src data source 1218 * @return e_model 1219 */ 1220 protected function _addData($key, $value = null, $override = true, $data_src = '_data') 1221 { 1222 if(is_array($key)) 1223 { 1224 foreach($key as $k => $v) 1225 { 1226 $this->_addData($k, $v, $override, $data_src); 1227 } 1228 return $this; 1229 } 1230 1231 if($override || !$this->_isData($key, $data_src)) 1232 { 1233 if(is_array($value)) 1234 { 1235 if(is_array($key)) 1236 { 1237 foreach($key as $k => $v) 1238 { 1239 $this->_addData($key.'/'.$k, $v, $override, $data_src); 1240 } 1241 } 1242 return $this; 1243 } 1244 $this->_setData($key, $value, false, $data_src); 1245 } 1246 return $this; 1247 } 1248 1249 /** 1250 * Unset data from the object from the given source. 1251 * $key can be a string only. Array will be ignored. 1252 * '/' inside the key will be treated as array path 1253 * if $key is null entire object will be reset 1254 * 1255 * @param string|null $key 1256 * @param string $data_src data source 1257 * @return e_model 1258 */ 1259 protected function _unsetData($key = null, $data_src = '_data') 1260 { 1261 if (null === $key) 1262 { 1263 if('_data' === $data_src && !empty($this->_data)) 1264 { 1265 $this->data_has_changed = true; 1266 } 1267 $this->{$data_src} = array(); 1268 return $this; 1269 } 1270 1271 $key = trim($key, '/'); 1272 if(strpos($key,'/')) 1273 { 1274 $keyArr = explode('/', $key); 1275 $data = &$this->{$data_src}; 1276 1277 $unskey = array_pop($keyArr); 1278 for ($i = 0, $l = count($keyArr); $i < $l; $i++) 1279 { 1280 $k = $keyArr[$i]; 1281 if (!isset($data[$k])) 1282 { 1283 return $this; //not found 1284 } 1285 $data = &$data[$k]; 1286 } 1287 if(is_array($data)) 1288 { 1289 if('_data' === $data_src && isset($data[$unskey])) 1290 { 1291 $this->data_has_changed = true; 1292 } 1293 unset($data[$unskey], $this->_parsed_keys[$data_src.'/'.$key]); 1294 } 1295 } 1296 else 1297 { 1298 if('_data' === $data_src && isset($this->{$data_src}[$key])) 1299 { 1300 $this->data_has_changed = true; 1301 } 1302 unset($this->{$data_src}[$key]); 1303 } 1304 return $this; 1305 } 1306 1307 /** 1308 * Unset single field from the object from the given source. Key is not parsed 1309 * 1310 * @param string $key 1311 * @param string $data_src data source 1312 * @return e_model 1313 */ 1314 protected function _unsetDataSimple($key, $data_src = '_data') 1315 { 1316 if('_data' === $data_src && isset($this->{$data_src}[$key])) 1317 { 1318 $this->data_has_changed = true; 1319 } 1320 unset($this->{$data_src}[$key]); 1321 return $this; 1322 } 1323 1324 /** 1325 * If $key is empty, checks whether there's any data in the object 1326 * Otherwise checks if the specified key is empty/set. 1327 * 1328 * @param string $key 1329 * @param string $data_src data source 1330 * @return boolean 1331 */ 1332 protected function _hasData($key = '', $data_src = '_data') 1333 { 1334 if (empty($key)) 1335 { 1336 return !empty($this->$data_src); 1337 } 1338 $value = $this->_getData($key, null, null, $data_src); 1339 return !empty($value); 1340 } 1341 1342 /** 1343 * Checks if the specified key is set 1344 * 1345 * @param string $key 1346 * @param string $data_src data source 1347 * @return boolean 1348 */ 1349 protected function _isData($key, $data_src = '_data') 1350 { 1351 return (null !== $this->_getData($key, null, null, $data_src)); 1352 } 1353 1354 /** 1355 * Add system message of type Information 1356 * 1357 * @param string $message 1358 * @param boolean $session [optional] 1359 * @return e_model 1360 */ 1361 public function addMessageInfo($message, $session = false) 1362 { 1363 e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_INFO, $session); 1364 return $this; 1365 } 1366 1367 /** 1368 * Add system message of type Success 1369 * 1370 * @param string $message 1371 * @param boolean $session [optional] 1372 * @return e_model 1373 */ 1374 public function addMessageSuccess($message, $session = false) 1375 { 1376 e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_SUCCESS, $session); 1377 return $this; 1378 } 1379 1380 /** 1381 * Add system message of type Warning 1382 * 1383 * @param string $message 1384 * @param boolean $session [optional] 1385 * @return e_model 1386 */ 1387 public function addMessageWarning($message, $session = false) 1388 { 1389 e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_WARNING, $session); 1390 return $this; 1391 } 1392 1393 /** 1394 * Add system message of type Error 1395 * 1396 * @param string $message 1397 * @param boolean $session [optional] 1398 * @param array $logData [optional] array('TABLE'=>'', 'ERROR'=>'') etc. 1399 * @return e_model 1400 */ 1401 public function addMessageError($message, $session = false, $logData = array()) 1402 { 1403 e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_ERROR, $session); 1404 1405 if(!empty($logData)) 1406 { 1407 e107::getAdminLog()->addArray($logData); 1408 } 1409 else 1410 { 1411 e107::getAdminLog()->addError($message,false); 1412 } 1413 1414 e107::getAdminLog()->save('ADMINUI_04', E_LOG_WARNING); 1415 1416 return $this; 1417 } 1418 1419 /** 1420 * Add system message of type Information 1421 * 1422 * @param string $message 1423 * @param boolean $session [optional] 1424 * @return e_model 1425 */ 1426 public function addMessageDebug($message, $session = false) 1427 { 1428 e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_DEBUG, $session); 1429 return $this; 1430 } 1431 1432 /** 1433 * Render System messages (if any) 1434 * 1435 * @param boolean $session store messages to session 1436 * @param boolean $reset reset errors 1437 * @return string 1438 */ 1439 public function renderMessages($session = false, $reset = true) 1440 { 1441 return e107::getMessage()->render($this->_message_stack, $session, $reset); 1442 } 1443 1444 /** 1445 * Move model System messages (if any) to the default eMessage stack 1446 * 1447 * @param boolean $session store messages to session 1448 * @return e_model 1449 */ 1450 public function setMessages($session = false) 1451 { 1452 e107::getMessage()->moveStack($this->_message_stack, 'default', false, $session); 1453 return $this; 1454 } 1455 1456 /** 1457 * Reset model System messages 1458 * 1459 * @param boolean|string $type E_MESSAGE_INFO | E_MESSAGE_SUCCESS | E_MESSAGE_WARNING | E_MESSAGE_WARNING | E_MESSAGE_DEBUG | false (all) 1460 * @param boolean $session reset also session messages 1461 * @return e_model 1462 */ 1463 public function resetMessages($type = false, $session = false) 1464 { 1465 e107::getMessage()->reset($type, $this->_message_stack, $session); 1466 return $this; 1467 } 1468 1469 /** 1470 * Set model message stack 1471 * @param string $stack_name 1472 * @return e_model 1473 */ 1474 public function setMessageStackName($stack_name) 1475 { 1476 $this->_message_stack = $stack_name; 1477 return $this; 1478 } 1479 1480 /** 1481 * Get model message stack name 1482 * @return string 1483 */ 1484 public function getMessageStackName() 1485 { 1486 return $this->_message_stack; 1487 } 1488 1489 /** 1490 * User defined model validation 1491 * Awaiting for child class implementation 1492 * 1493 */ 1494 public function verify() 1495 { 1496 } 1497 1498 /** 1499 * Model validation 1500 * @see e_model_admin 1501 */ 1502 public function validate() 1503 { 1504 } 1505 1506 /** 1507 * Generic load data from DB 1508 * @param mixed $id 1509 * @param boolean $force 1510 * @return e_model 1511 */ 1512 public function load($id = null, $force = false) 1513 { 1514 1515 1516 if(!$force && $this->getId()) 1517 { 1518 return $this; 1519 } 1520 1521 if($force) 1522 { 1523 $this->setData(array()) 1524 ->_clearCacheData(); 1525 } 1526 if($id) $id = e107::getParser()->toDB($id); 1527 if(!$id && !$this->getParam('db_query')) 1528 { 1529 return $this; 1530 } 1531 1532 $cached = $this->_getCacheData(); 1533 if($cached !== false) 1534 { 1535 $this->setData($cached); 1536 return $this; 1537 } 1538 1539 $sql = e107::getDb(); 1540 $qry = str_replace('{ID}', $id, $this->getParam('db_query')); 1541 if($qry) 1542 { 1543 $res = $sql->gen($qry, $this->getParam('db_debug') ? true : false); 1544 } 1545 else 1546 { 1547 if(!is_numeric($id)) $id = "'{$id}'"; 1548 1549 $res = $sql->select( 1550 $this->getModelTable(), 1551 $this->getParam('db_fields', '*'), 1552 $this->getFieldIdName().'='.$id.' '.trim($this->getParam('db_where', '')), 1553 'default', 1554 ($this->getParam('db_debug') ? true : false) 1555 ); 1556 } 1557 1558 1559 if($res) 1560 { 1561 $this->setData($sql->fetch()); 1562 } 1563 1564 if($sql->getLastErrorNumber()) 1565 { 1566 $this->addMessageDebug('SQL error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText()); 1567 $this->addMessageDebug($sql->getLastQuery()); 1568 } 1569 else 1570 { 1571 1572 $this->_setCacheData(); 1573 } 1574 1575 return $this; 1576 } 1577 1578 /** 1579 * Retrieve system cache (if any) 1580 * @return array|false 1581 */ 1582 protected function _getCacheData() 1583 { 1584 if(!$this->isCacheEnabled()) 1585 { 1586 return false; 1587 } 1588 1589 $cached = e107::getCache()->retrieve_sys($this->getCacheString(true), false, $this->_cache_force); 1590 if(false !== $cached) 1591 { 1592 return e107::unserialize($cached); 1593 } 1594 1595 return false; 1596 } 1597 1598 /** 1599 * Set system cache if enabled for the model 1600 * @return e_model 1601 */ 1602 protected function _setCacheData() 1603 { 1604 if(!$this->isCacheEnabled()) 1605 { 1606 return $this; 1607 } 1608 e107::getCache()->set_sys($this->getCacheString(true), $this->toString(false), $this->_cache_force, false); 1609 return $this; 1610 } 1611 1612 /** 1613 * Clrear system cache if enabled for the model 1614 * @return e_model 1615 */ 1616 protected function _clearCacheData() 1617 { 1618 if(!$this->isCacheEnabled(false)) 1619 { 1620 return $this; 1621 } 1622 e107::getCache()->clear_sys($this->getCacheString(true), false); 1623 return $this; 1624 } 1625 1626 /** 1627 * Clrear system cache (public proxy) if enabled for the model 1628 * @return e_model 1629 */ 1630 public function clearCache() 1631 { 1632 return $this->_clearCacheData(); 1633 } 1634 1635 /** 1636 * Check if cache is enabled for the current model 1637 * @param boolean $checkId check if there is model ID 1638 * @return boolean 1639 */ 1640 public function isCacheEnabled($checkId = true) 1641 { 1642 return (null !== $this->getCacheString() && (!$checkId || $this->getId())); 1643 } 1644 1645 /** 1646 * Get model cache string 1647 * @param boolean $replace try to add current model ID (replace destination is {ID}) 1648 * @return string 1649 */ 1650 public function getCacheString($replace = false) 1651 { 1652 return ($replace ? str_replace('{ID}', $this->getId(), $this->_cache_string) : $this->_cache_string); 1653 } 1654 1655 /** 1656 * Set model cache string 1657 * @param string $str 1658 * @return e_model 1659 */ 1660 public function setCacheString($str) 1661 { 1662 $this->_cache_string = $str; 1663 return $this; 1664 } 1665 1666 /** 1667 * Save data to DB 1668 * Awaiting for child class implementation 1669 * @see e_model_admin 1670 */ 1671 public function save() 1672 { 1673 } 1674 1675 /** 1676 * Delete DB record 1677 * Awaiting for child class implementation 1678 * @see e_model_admin 1679 */ 1680 public function delete($ids, $destroy = true, $session_messages = false) 1681 { 1682 } 1683 1684 /** 1685 * Create new DB recorrd 1686 * Awaiting for child class implementation 1687 * @see e_model_admin 1688 */ 1689 public function create() 1690 { 1691 } 1692 1693 /** 1694 * Insert data to DB 1695 * Awaiting for child class implementation 1696 * @see e_model_admin 1697 */ 1698 protected function dbInsert() 1699 { 1700 } 1701 1702 /** 1703 * Update DB data 1704 * Awaiting for child class implementation 1705 * @see e_model_admin 1706 */ 1707 protected function dbUpdate($force = false, $session_messages = false) 1708 { 1709 } 1710 1711 /** 1712 * Replace DB record 1713 * Awaiting for child class implementation 1714 * @see e_model_admin 1715 */ 1716 protected function dbReplace() 1717 { 1718 } 1719 1720 /** 1721 * Delete DB data 1722 * Awaiting for child class implementation 1723 * @see e_model_admin 1724 */ 1725 protected function dbDelete() 1726 { 1727 } 1728 1729 /** 1730 * Set parameter array 1731 * Core implemented: 1732 * - db_query: string db query to be passed to load() ($sql->gen()) 1733 * - db_query 1734 * - db_fields 1735 * - db_where 1736 * - db_debug 1737 * - model_class: e_tree_model class/subclasses - string class name for creating nodes inside default load() method 1738 * - clearModelCache: e_tree_model class/subclasses - clear cache per node after successful DB operation 1739 * - noCacheStringModify: e_tree_model class/subclasses - do not add additional md5 sum to tree cache string 1740 * @param array $params 1741 * @return e_model|e_tree_model 1742 */ 1743 public function setParams(array $params) 1744 { 1745 parent::setParams($params); 1746 return $this; 1747 } 1748 1749 1750 1751 /** 1752 * Render model data, all 'sc_*' methods will be recongnized 1753 * as shortcodes. 1754 * 1755 * @param string $template 1756 * @param boolean $parsesc parse external shortcodes, default is true 1757 * @param e_vars $eVars simple parser data 1758 * @return string parsed template 1759 */ 1760 public function toHTML($template, $parsesc = true, $eVars = null) 1761 { 1762 return e107::getParser()->parseTemplate($template, $parsesc, $this, $eVars); 1763 } 1764 1765 /** 1766 * Export a Model configuration 1767 * @return string 1768 */ 1769 public function toXML() 1770 { 1771 $ret = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"; 1772 $ret .= "<e107Export type=\"model\" version=\"1.0\" timestamp=\"".time()."\" >\n"; 1773 1774 $ret .= "\t<data>\n"; 1775 // TODO - handle multi dimensional arrays (already possible - field1/field2?), method toXMLValue($value, $type) 1776 foreach ($this->getDataFields() as $field => $type) 1777 { 1778 $ret .= "\t\t<field name=\"{$field}\" type=\"{$type}\">"; 1779 $ret .= $type == 'str' || $type == 'string' ? "<![CDATA[".$this->getData($field)."]]>" : $this->getData($field); 1780 $ret .= "</field>\n"; 1781 } 1782 $ret .= "\t</data>\n"; 1783 1784 $ret .= "</e107Export>"; 1785 return $ret; 1786 } 1787 1788 /** 1789 * Try to convert string to a number 1790 * Shoud fix locale related troubles 1791 * 1792 * @param string $value 1793 * @return integer|float 1794 */ 1795 // moved to e_parse 1796 // public function toNumber($value) 1797 // { 1798 // $larr = localeconv(); 1799 // $search = array( 1800 // $larr['decimal_point'], 1801 // $larr['mon_decimal_point'], 1802 // $larr['thousands_sep'], 1803 // $larr['mon_thousands_sep'], 1804 // $larr['currency_symbol'], 1805 // $larr['int_curr_symbol'] 1806 // ); 1807 // $replace = array('.', '.', '', '', '', ''); 1808 1809 // return str_replace($search, $replace, $value); 1810 // } 1811 1812 /** 1813 * Convert object data to a string 1814 * 1815 * @param boolean $AddSlashes 1816 * @param string $key optional, if set method will return corresponding value as a string 1817 * @return string 1818 */ 1819 public function toString($AddSlashes = true, $key = null) 1820 { 1821 if (null !== $key) 1822 { 1823 $value = $this->getData($key); 1824 if(is_array($value)) 1825 { 1826 return e107::getArrayStorage()->WriteArray($value, $AddSlashes); 1827 } 1828 return (string) $value; 1829 } 1830 return (string) e107::getArrayStorage()->WriteArray($this->toArray(), $AddSlashes); 1831 } 1832 1833 public function destroy() 1834 { 1835 $this->_data = array(); 1836 $this->_params = array(); 1837 $this->_data_fields = array(); 1838 $this->_parsed_keys = array(); 1839 $this->_db_table = $this->_field_id = ''; 1840 $this->data_has_changed = false; 1841 } 1842 1843 /** 1844 * Disable Magic setter 1845 */ 1846 public function __set($key, $value) 1847 { 1848 } 1849 1850 /** 1851 * Disable Magic getter 1852 */ 1853 public function __get($key) 1854 { 1855 } 1856 1857 /** 1858 * Disable 1859 */ 1860 public function __isset($key) 1861 { 1862 } 1863 1864 /** 1865 * Disable 1866 */ 1867 public function __unset($key) 1868 { 1869 } 1870} 1871 1872/** 1873 * Base e107 Front Model class interface 1874 * 1875 * Some important points: 1876 * - model data should be always in toDB() format: 1877 * - retrieved direct from DB 1878 * - set & sanitized via setPostedData()->mergePostedData() 1879 * - manually sanitized before passed to model setter (set(), setData(), add(), addData(), etc.) methods 1880 * - $_data_fields property is important, it tells to sanitize() method how to sanitize posted data 1881 * - if $_data_fields is missing, sanitize() will call internally e107::getParser()->toDB() on the data 1882 * - sanitize() is triggered by default on mergePostedData() and mergeData() methods 1883 * - mergePostedData() and mergeData() methods will filter posted/passed data against (in this order): 1884 * - getValidator()->getValidData() if true is passed as validate parameter (currently disabled, gather feedback) 1885 * - $_data_fields if true is passed as sanitize parameter 1886 * - toSqlQuery() needs $_data_fields and $_field_id to work proper, $_FIELD_TYPES is optional but recommended (faster SQL queries) 1887 * - result array from toSqlQuery() call will be filtered against $_data_fields 1888 * - in almost every case $_FIELD_TYPES shouldn't contain 'escape' and 'todb' - dont't forget you are going to pass already sanitized data (see above) 1889 * - most probably $_FIELD_TYPES will go in the future, $_data_fields alone could do the job 1890 * - default db related methods (save(), dbUpdate(), etc.) need $_db_table 1891 * 1892 * @package e107 1893 * @category e107_handlers 1894 * @version $Id$ 1895 * @author SecretR 1896 * @copyright Copyright (C) 2008-2010 e107 Inc. 1897 */ 1898class e_front_model extends e_model 1899{ 1900 /** 1901 * Posted data 1902 * Back-end related 1903 * 1904 * @var array 1905 */ 1906 protected $_posted_data = array(); 1907 1908 1909 /** 1910 * DB format array - see db::_getTypes() and db::_getFieldValue() (mysql_class.php) 1911 * for example 1912 * 1913 * This can/should be overwritten by extending the class 1914 * 1915 * @var array 1916 */ 1917 protected $_FIELD_TYPES = array(); 1918 1919 /** 1920 * Validation structure - see {@link e_validator::$_required_rules} for 1921 * more information about the array format. 1922 * Used in {@link validate()} method. 1923 * TODO - check_rules (see e_validator::$_optional_rules) 1924 * This can/should be overwritten by extending the class. 1925 * 1926 * @var array 1927 */ 1928 protected $_validation_rules = array(); 1929 1930 protected $_optional_rules = array(); 1931 1932 /** 1933 * @var integer Last SQL error number 1934 */ 1935 protected $_db_errno = 0; 1936 1937 /** 1938 * @var string Last SQL error message 1939 */ 1940 protected $_db_errmsg = ''; 1941 1942 /** 1943 * @var string Last SQL query 1944 */ 1945 protected $_db_qry = ''; 1946 1947 /** 1948 * Validator object 1949 * 1950 * @var e_validator 1951 */ 1952 protected $_validator = null; 1953 1954 /** 1955 * @return array 1956 */ 1957 public function getValidationRules() 1958 { 1959 return $this->_validation_rules; 1960 } 1961 1962 /** 1963 * Set object validation rules if $_validation_rules array is empty 1964 * 1965 * @param array $vrules 1966 * @return e_front_model 1967 */ 1968 public function setValidationRules(array $vrules, $force = false) 1969 { 1970 if($force || empty($this->_validation_rules)) 1971 { 1972 $this->_validation_rules = $vrules; 1973 } 1974 return $this; 1975 } 1976 1977 /** 1978 * @return array 1979 */ 1980 public function getOptionalRules() 1981 { 1982 return $this->_optional_rules; 1983 } 1984 1985 /** 1986 * @param array $rules 1987 * @return e_front_model 1988 */ 1989 public function setOptionalRules(array $rules) 1990 { 1991 $this->_optional_rules = $rules; 1992 return $this; 1993 } 1994 1995 /** 1996 * Set object validation rules if $_validation_rules array is empty 1997 * 1998 * @param string $field 1999 * @param array $rule 2000 * @param boolean $required 2001 * @return e_front_model 2002 */ 2003 public function setValidationRule($field, $rule, $required = true) 2004 { 2005 $pname = $required ? '_validation_rules' : '_optional_rules'; 2006 $rules = &$this->$pname; 2007 $rules[$field] = $rule; 2008 2009 return $this; 2010 } 2011 2012 /** 2013 * Predefined data fields types, passed to DB handler 2014 * @return array 2015 */ 2016 public function getDbTypes() 2017 { 2018 return ($this->_FIELD_TYPES ? $this->_FIELD_TYPES : $this->getDataFields()); 2019 } 2020 2021 /** 2022 * Predefined data fields types, passed to DB handler 2023 * 2024 * @param array $field_types 2025 * @return e_front_model 2026 */ 2027 public function setDbTypes($field_types) 2028 { 2029 $this->_FIELD_TYPES = $field_types; 2030 return $this; 2031 } 2032 2033 /** 2034 * Auto field type definitions 2035 * Disabled for now, it should auto-create _data_types 2036 * @param boolean $force 2037 * @return boolean 2038 */ 2039// public function setFieldTypeDefs($force = false) 2040// { 2041// if($force || !$this->getFieldTypes()) 2042// { 2043// $ret = e107::getDb()->getFieldDefs($this->getModelTable()); 2044// if($ret) 2045// { 2046// foreach ($ret as $k => $v) 2047// { 2048// if('todb' == $v) 2049// { 2050// $ret[$k] = 'string'; 2051// } 2052// } 2053// $this->setFieldTypes($ret); 2054// return true; 2055// } 2056// } 2057// return false; 2058// } 2059 2060 /** 2061 * Retrieves data from the object ($_posted_data) without 2062 * key parsing (performance wise, prefered when possible) 2063 * 2064 * @see _getDataSimple() 2065 * @param string $key 2066 * @param mixed $default 2067 * @return mixed 2068 */ 2069 public function getPosted($key, $default = null) 2070 { 2071 return $this->_getDataSimple((string) $key, $default, '_posted_data'); 2072 } 2073 2074 /** 2075 * Retrieves data from the object ($_posted_data) 2076 * If $key is empty, return all object posted data 2077 * @see _getData() 2078 * @param string $key 2079 * @param mixed $default 2080 * @param integer $index 2081 * @return mixed 2082 */ 2083 public function getPostedData($key = '', $default = null, $index = null) 2084 { 2085 return $this->_getData($key, $default, $index, '_posted_data'); 2086 } 2087 2088 /** 2089 * Search for requested data from available sources in this order: 2090 * - posted data 2091 * - default object data 2092 * - passed default value 2093 * 2094 * Use this method inside forms 2095 * 2096 * @param string $key 2097 * @param string $default 2098 * @param integer $index 2099 * @return string 2100 */ 2101 public function getIfPosted($key, $default = '', $index = null) 2102 { 2103 $d = $this->getDataFields(); 2104 2105 if(!empty($d[$key]) && ($d[$key] == 'array')) 2106 { 2107 return e107::unserialize($this->getData((string) $key, $default, $index)); 2108 } 2109 2110 $posted = $this->getPostedData((string) $key, null, $index); 2111 if(null !== $posted) 2112 { 2113 // FIXED - double post_toFom() and toDB(post_toForm()) problems 2114 // setPosted|setPostedData|addPostedData methods are storing RAW data now 2115 return e107::getParser()->post_toForm($posted); 2116 } 2117 return e107::getParser()->toForm($this->getData((string) $key, $default, $index)); 2118 } 2119 2120 /** 2121 * Overwrite posted data in the object for a single field. Key is not parsed. 2122 * Public proxy of {@link _setDataSimple()} 2123 * Use this method to store data from non-trustable sources (e.g. _POST) - it doesn't overwrite 2124 * the original object data 2125 * 2126 * @param string $key 2127 * @param mixed $value 2128 * @param boolean $strict update only 2129 * @return e_front_model 2130 */ 2131 public function setPosted($key, $value, $strict = false) 2132 { 2133 return $this->_setDataSimple($key, $value, $strict, '_posted_data'); 2134 } 2135 2136 /** 2137 * Overwrite posted data in the object. Key is parsed (multidmensional array support). 2138 * Public proxy of {@link _setData()} 2139 * Use this method to store data from non-trustable sources (e.g. _POST) - it doesn't overwrite 2140 * the original object data 2141 * 2142 * @param string|array $key 2143 * @param mixed $value 2144 * @param boolean $strict update only 2145 * @return e_front_model 2146 */ 2147 public function setPostedData($key, $value = null, $strict = false) 2148 { 2149 return $this->_setData($key, $value, $strict, '_posted_data'); 2150 } 2151 2152 /** 2153 * Add data to the object. 2154 * Retains existing data in the object. 2155 * Public proxy of {@link _addData()} 2156 * 2157 * If $override is false, data will be updated only (check against existing data) 2158 * 2159 * @param string|array $key 2160 * @param mixed $value 2161 * @param boolean $override override existing data 2162 * @return e_front_model 2163 */ 2164 public function addPostedData($key, $value = null, $override = true) 2165 { 2166 return $this->_addData($key, $value, $override, '_posted_data'); 2167 } 2168 2169 /** 2170 * Unset single posted data field from the object. 2171 * Public proxy of {@link _unsetDataSimple()} 2172 * 2173 * @param string $key 2174 * @return e_front_model 2175 */ 2176 public function removePosted($key) 2177 { 2178 return $this->_unsetDataSimple($key, '_posted_data'); 2179 } 2180 2181 /** 2182 * Unset posted data from the object. 2183 * $key can be a string only. Array will be ignored. 2184 * '/' inside the key will be treated as array path 2185 * if $key is null entire object will be reset 2186 * 2187 * Public proxy of {@link _unsetData()} 2188 * 2189 * @param string|null $key 2190 * @return e_front_model 2191 */ 2192 public function removePostedData($key = null) 2193 { 2194 return $this->_unsetData($key, '_posted_data'); 2195 } 2196 2197 /** 2198 * Check if given key exists and non-empty in the posted data array 2199 * @param string $key 2200 * @return boolean 2201 */ 2202 public function hasPosted($key) 2203 { 2204 return $this->_hasData($key, '_posted_data'); 2205 } 2206 2207 /** 2208 * Check if posted data is empty 2209 * @return boolean 2210 */ 2211 public function hasPostedData() 2212 { 2213 return $this->_hasData('', '_posted_data'); 2214 } 2215 2216 /** 2217 * Check if given key exists in the posted data array 2218 * 2219 * @param string $key 2220 * @return boolean 2221 */ 2222 public function isPosted($key) 2223 { 2224 return (isset($this->_posted_data[$key])); 2225 } 2226 2227 /** 2228 * Check if given key exists in the posted data array ($key us parsed) 2229 * 2230 * @param string $key 2231 * @return boolean 2232 */ 2233 public function isPostedData($key) 2234 { 2235 return $this->_isData($key, '_posted_data'); 2236 } 2237 2238 /** 2239 * Compares posted data vs object data 2240 * 2241 * @param string $field 2242 * @param boolean $strict compare variable type as well 2243 * @return boolean 2244 */ 2245 public function dataHasChangedFor($field, $strict = false) 2246 { 2247 $newData = $this->getData($field); 2248 $postedData = $this->getPostedData($field); 2249 return ($strict ? $newData !== $postedData : $newData != $postedData); 2250 } 2251 2252 /** 2253 * @return boolean 2254 */ 2255 public function dataHasChanged() 2256 { 2257 return $this->data_has_changed; 2258 } 2259 2260 /** 2261 * Merge posted data with the object data 2262 * Should be used on edit/update/create record (back-end) 2263 * Retrieved for copy Posted data will be removed (no matter if copy is successfull or not) 2264 * 2265 * If $strict is true, only existing object data will be copied (update) 2266 * If $validate is true, data will be copied only after successful validation 2267 * 2268 * @param boolean $strict 2269 * @param boolean $sanitize sanitize posted data before move it to the object data 2270 * @param boolean $validate perform validation check 2271 * @return e_front_model 2272 */ 2273 public function mergePostedData($strict = true, $sanitize = true, $validate = true) 2274 { 2275 if(!$this->hasPostedData() || ($validate && !$this->validate())) 2276 { 2277 return $this; 2278 } 2279 2280 2281 $oldData = $this->getData(); 2282// $this->addMessageDebug("OLDD".print_a($oldData,true)); 2283 2284 2285 $data = $this->getPostedData(); 2286 2287 $valid_data = $validate ? $this->getValidator()->getValidData() : array(); 2288 2289 if($sanitize) 2290 { 2291 // search for db_field types 2292 if($this->getDataFields()) 2293 { 2294 $data = $this->sanitize($data); 2295 } 2296 else //no db field types, use toDB() 2297 { 2298 $data = e107::getParser()->toDB($data); 2299 } 2300 } 2301 2302 // $newData = $this->getPostedData(); 2303 e107::getAdminLog()->addArray($data,$oldData); 2304 // $this->addMessageDebug("NEWD".print_a($data,true)); 2305 2306 $tp = e107::getParser(); 2307 foreach ($data as $field => $dt) 2308 { 2309 // get values form validated array when possible 2310 // we need it because of advanced validation methods e.g. 'compare' 2311 // FIX - security issue, toDb required 2312 if(isset($valid_data[$field])) $dt = $tp->toDb($valid_data[$field]); 2313 2314 $this->setData($field, $dt, $strict) 2315 ->removePostedData($field); 2316 } 2317 2318 2319 2320 2321 return $this; 2322 } 2323 2324 /** 2325 * Merge passed data array with the object data 2326 * Should be used on edit/update/create record (back-end) 2327 * 2328 * If $strict is true, only existing object data will be copied (update) 2329 * If $validate is true, data will be copied only after successful validation 2330 * 2331 * @param array $src_data 2332 * @param boolean $sanitize 2333 * @param boolean $validate perform validation check 2334 * @return e_front_model 2335 */ 2336 public function mergeData(array $src_data, $strict = true, $sanitize = true, $validate = true) 2337 { 2338 //FIXME 2339 if(!$src_data || ($validate && !$this->validate($src_data))) 2340 { 2341 return $this; 2342 } 2343 2344 /* Wrong? 2345 // retrieve only valid data 2346 if($validate) 2347 { 2348 $src_data = $this->getValidator()->getValidData(); 2349 }*/ 2350 2351 if($sanitize) 2352 { 2353 // search for db_field types 2354 if($this->getDataFields()) 2355 { 2356 $src_data = $this->sanitize($src_data); 2357 } 2358 else //no db field types, use toDB() 2359 { 2360 $src_data = e107::getParser()->toDB($src_data); 2361 } 2362 } 2363 2364 2365 2366 foreach ($src_data as $key => $value) 2367 { 2368 $this->setData($key, $value, $strict); 2369 } 2370 2371 2372 2373 return $this; 2374 } 2375 2376 /** 2377 * Validate posted data: 2378 * 1. validate posted data against object validation rules 2379 * 2. add validation errors to the object if any 2380 * 3. return true for valid and false for non-valid data 2381 * 2382 * @param array $data optional - data for validation, defaults to posted data 2383 * @return boolean 2384 */ 2385 public function validate($data = null) 2386 { 2387 if(!$this->getValidationRules()) 2388 { 2389 return true; 2390 } 2391 if(null === $data) 2392 { 2393 $data = $this->getPostedData(); 2394 } 2395 2396 // New param to control validate process - useful when part of the data is going to be updated 2397 // Use it with cautious!!! 2398 $availableOnly = false; 2399 if($this->getParam('validateAvailable')) 2400 { 2401 $availableOnly = true; 2402 $this->setParam('validateAvailable', null); // reset it 2403 } 2404 2405 return $this->getValidator()->validate($data, $availableOnly); 2406 } 2407 2408 /** 2409 * User defined model validation 2410 * Awaiting for child class implementation 2411 * 2412 */ 2413 public function verify() 2414 { 2415 } 2416 2417 /** 2418 * @return e_validator 2419 */ 2420 public function getValidator() 2421 { 2422 if(null === $this->_validator) 2423 { 2424 $this->_validator = e107::getObject('e_validator'); 2425 $this->_validator->setRules($this->getValidationRules()) 2426 ->setOptionalRules($this->getOptionalRules()) 2427 ->setMessageStack($this->_message_stack.'_validator'); 2428 //TODO - optional check rules 2429 } 2430 return $this->_validator; 2431 } 2432 2433 /** 2434 * Add custom validation message. 2435 * $field_type and $error_code will be inserted via $tp->lanVars() 2436 * in the $message string 2437 * Example: 2438 * <code> 2439 * $model->addValidationError('Custom error message [#%d] for %s', 'My Field', 1000); 2440 * //produces 'Custom error message [#1000] for My Field' 2441 * </code> 2442 * 2443 * @param string $message 2444 * @param string $field_title [optional] 2445 * @param integer $error_code [optional] 2446 * @return object 2447 */ 2448 public function addValidationError($message, $field_title = '', $error_code = 0) 2449 { 2450 $this->getValidator()->addValidateMessage($field_title, $error_code, $message)->setIsValidData(false); 2451 return $this; 2452 } 2453 2454 /** 2455 * Render validation errors (if any) 2456 * 2457 * @param boolean $session store messages to session 2458 * @param boolean $reset reset errors 2459 * @return string 2460 */ 2461 public function renderValidationErrors($session = false, $reset = true) 2462 { 2463 return $this->getValidator()->renderValidateMessages($session, $reset); 2464 } 2465 2466 /** 2467 * Render System messages (if any) 2468 * 2469 * @param boolean $validation render validation messages as well 2470 * @param boolean $session store messages to session 2471 * @param boolean $reset reset errors 2472 * @return string 2473 */ 2474 public function renderMessages($validation = true, $session = false, $reset = true) 2475 { 2476 if($validation) 2477 { 2478 e107::getMessage()->moveStack($this->_message_stack.'_validator', $this->_message_stack, false, $session); 2479 } 2480 return parent::renderMessages($session, $reset); 2481 } 2482 2483 /** 2484 * Move model System messages (if any) to the default eMessage stack 2485 * 2486 * @param boolean $session store messages to session 2487 * @param boolean $validation move validation messages as well 2488 * @return e_front_model 2489 */ 2490 public function setMessages($session = false, $validation = true) 2491 { 2492 if($validation) 2493 { 2494 e107::getMessage()->moveStack($this->_message_stack.'_validator', 'default', false, $session); 2495 } 2496 parent::setMessages($session); 2497 return $this; 2498 } 2499 2500 /** 2501 * Reset model System messages 2502 * 2503 * @param boolean|string $type E_MESSAGE_INFO | E_MESSAGE_SUCCESS | E_MESSAGE_WARNING | E_MESSAGE_WARNING | E_MESSAGE_DEBUG | false (all) 2504 * @param boolean $session reset session messages 2505 * @param boolean $validation reset validation messages as well 2506 * @return e_front_model 2507 */ 2508 public function resetMessages($type = false, $session = false, $validation = false) 2509 { 2510 if($validation) 2511 { 2512 e107::getMessage()->reset($type, $this->_message_stack.'_validator', $session); 2513 } 2514 parent::resetMessages($type, $session); 2515 return $this; 2516 } 2517 2518 /** 2519 * @return boolean 2520 */ 2521 public function hasValidationError() 2522 { 2523 return $this->getValidator()->isValid(); 2524 } 2525 2526 /** 2527 * @return boolean 2528 */ 2529 public function hasSqlError() 2530 { 2531 return !empty($this->_db_errno); 2532 } 2533 2534 /** 2535 * @return integer last mysql error number 2536 */ 2537 public function getSqlErrorNumber() 2538 { 2539 return $this->_db_errno; 2540 } 2541 2542 /** 2543 * @return string last mysql error message 2544 */ 2545 public function getSqlError() 2546 { 2547 return $this->_db_errmsg; 2548 } 2549 2550 /** 2551 * @return string last mysql error message 2552 */ 2553 public function getSqlQuery() 2554 { 2555 return $this->_db_qry; 2556 } 2557 2558 /** 2559 * @return boolean 2560 */ 2561 public function hasError() 2562 { 2563 return ($this->hasValidationError() || $this->hasSqlError()); 2564 } 2565 2566 /** 2567 * Generic load data from DB 2568 * @param boolean $force 2569 * @return e_front_model 2570 */ 2571 public function load($id=null, $force = false) 2572 { 2573 parent::load($id, $force); 2574 2575 $sql = e107::getDb(); 2576 $this->_db_errno = $sql->getLastErrorNumber(); 2577 $this->_db_errmsg = $sql->getLastErrorText(); 2578 $this->_db_qry = $sql->getLastQuery(); 2579 2580 if($this->_db_errno) 2581 { 2582 $data = array( 2583 'TABLE' => $this->getModelTable(), 2584 'error_no' => $this->_db_errno, 2585 'error_msg' => $this->_db_errmsg, 2586 'qry' => $this->_db_qry, 2587 'url' => e_REQUEST_URI, 2588 ); 2589 2590 2591 $this->addMessageError('SQL Select Error', false, $data); //TODO - Lan 2592 // already done by the parent 2593 //$this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$sql->getLastErrorText()); 2594 } 2595 2596 2597 return $this; 2598 } 2599 2600 2601 /** 2602 * Build query array to be used with db methods (db_Update, db_Insert, db_Replace) 2603 * 2604 * @param string $force [optional] force action - possible values are create|update|replace 2605 * @return array db query 2606 */ 2607 public function toSqlQuery($force = '') 2608 { 2609 $qry = array(); 2610 2611 if($force) 2612 { 2613 $action = $force; 2614 } 2615 else 2616 { 2617 $action = $this->getId() ? 'update' : 'create'; 2618 } 2619 2620 $qry['_FIELD_TYPES'] = $this->_FIELD_TYPES; //DB field types are optional 2621 2622 // support for tables with no auto-increment PK 2623 $id = $this->getId(); 2624 $qry['data'][$this->getFieldIdName()] = $id; 2625 2626 //XXX This check is done in _setModel() of admin-ui. NULL below will break MySQL strict. 2627 // Allow admin config to specify the best data type. 2628 /* 2629 if($action == 'create' && !$id) $qry['_FIELD_TYPES'][$this->getFieldIdName()] = 'NULL'; 2630 elseif(is_numeric($id)) $qry['_FIELD_TYPES'][$this->getFieldIdName()] = 'integer'; 2631 else $qry['_FIELD_TYPES'][$this->getFieldIdName()] = 'string'; 2632 */ 2633 2634 foreach ($this->_data_fields as $key => $type) 2635 { 2636 2637 if(!isset($qry['_FIELD_TYPES'][$key])) 2638 { 2639 $qry['_FIELD_TYPES'][$key] = $type; //_FIELD_TYPES much more optional now... 2640 } 2641 2642 if($qry['_FIELD_TYPES'][$key] == 'set') //new 'set' type, could be moved in mysql handler now 2643 { 2644 $qry['_FIELD_TYPES'][$key] = 'str'; 2645 if(is_array($this->getData($key))) $this->setData($key, implode(',', $this->getData($key))); 2646 } 2647 $qry['data'][$key] = $this->getData($key); 2648 2649 } 2650 2651 switch($action) 2652 { 2653 case 'create': 2654 //$qry['data'][$this->getFieldIdName()] = NULL; 2655 break; 2656 case 'replace': 2657 $qry['_REPLACE'] = true; 2658 break; 2659 2660 case 'update': 2661 unset($qry['data'][$this->getFieldIdName()]); 2662 if(is_numeric($id)) $id = intval($id); 2663 else $id = "'".e107::getParser()->toDB($id)."'"; 2664 $qry['WHERE'] = $this->getFieldIdName().'='.$id; 2665 break; 2666 } 2667 2668 if(E107_DEBUG_LEVEL == E107_DBG_SQLQUERIES) 2669 { 2670 $this->addMessageDebug('SQL Qry: '.print_a($qry,true), null); 2671 } 2672 return $qry; 2673 } 2674 2675 /** 2676 * Sanitize value based on its db field type ($_data_fields), 2677 * method will return null only if db field rule is not found. 2678 * If $value is null, it'll be retrieved from object posted data 2679 * If $key is an array, $value is omitted. 2680 * 2681 * NOTE: If $key is not found in object's _data_fields array, null is returned 2682 * 2683 * @param mixed $key string key name or array data to be sanitized 2684 * @param mixed $value 2685 * @return mixed sanitized $value or null on failure 2686 */ 2687 public function sanitize($key, $value = null) 2688 { 2689 $tp = e107::getParser(); 2690 if(is_array($key)) 2691 { 2692 $ret = array(); 2693 foreach ($key as $k=>$v) 2694 { 2695 if(isset($this->_data_fields[$k])) 2696 { 2697 $ret[$k] = $this->sanitize($k, $v); 2698 } 2699 } 2700 return $ret; 2701 } 2702 2703 if(!isset($this->_data_fields[$key])) 2704 { 2705 return null; 2706 } 2707 $type = $this->_data_fields[$key]; 2708 if(null === $value) 2709 { 2710 $value = $this->getPostedData($key); 2711 } 2712 2713 2714 switch ($type) 2715 { 2716 case 'int': 2717 case 'integer': 2718 //return intval($this->toNumber($value)); 2719 return intval($tp->toNumber($value)); 2720 break; 2721 2722 case 'safestr': 2723 return $tp->filter($value); 2724 break; 2725 2726 case 'str': 2727 case 'string': 2728 case 'array': 2729 $type = $this->getFieldInputType($key); 2730 return $tp->toDB($value, false, false, 'model', array('type'=>$type, 'field'=>$key)); 2731 break; 2732 2733 case 'json': 2734 if(empty($value)) 2735 { 2736 return null; 2737 } 2738 return e107::serialize($value,'json'); 2739 break; 2740 2741 case 'code': 2742 return $tp->toDB($value, false, false, 'pReFs'); 2743 break; 2744 2745 case 'float': 2746 // return $this->toNumber($value); 2747 return $tp->toNumber($value); 2748 break; 2749 2750 case 'bool': 2751 case 'boolean': 2752 return ($value ? true : false); 2753 break; 2754 2755 case 'model': 2756 return $value->mergePostedData(false, true, true); 2757 break; 2758 2759 case 'null': 2760 return ($value ? $tp->toDB($value) : null); 2761 break; 2762 } 2763 2764 return null; 2765 } 2766 2767 2768 2769 public function destroy() 2770 { 2771 parent::destroy(); 2772 $this->_validator = null; 2773 $this->_validation_rules = array(); 2774 $this->_db_errno = null; 2775 $this->_posted_data = array(); 2776 $this->data_has_changed = array(); 2777 $this->_FIELD_TYPES = array(); 2778 } 2779 2780 2781 /** 2782 * Update DB data 2783 * 2784 * @param boolean $force force query even if $data_has_changed is false 2785 * @param boolean $session_messages to use or not session to store system messages 2786 */ 2787 protected function dbUpdate($force = false, $session_messages = false) 2788 { 2789 $this->_db_errno = 0; 2790 $this->_db_errmsg = ''; 2791 $this->_db_qry = ''; 2792 2793 // $this->getData(); 2794 // $this->getPostedData(); 2795 2796 2797 if($this->hasError()) return false; 2798 2799 if(!$this->data_has_changed && $force === false) 2800 { 2801 $this->addMessageInfo(LAN_NO_CHANGE); 2802 return 0; 2803 } 2804 2805 $sql = e107::getDb(); 2806 $qry = $this->toSqlQuery('update'); 2807 $table = $this->getModelTable(); 2808 2809 $res = $sql->update($table, $qry, $this->getParam('db_debug', false)); 2810 $this->_db_qry = $sql->getLastQuery(); 2811 if(!$res) 2812 { 2813 $this->_db_errno = $sql->getLastErrorNumber(); 2814 $this->_db_errmsg = $sql->getLastErrorText(); 2815 2816 if($this->_db_errno) 2817 { 2818 $data = array( 2819 'TABLE' => $table, 2820 'error_no' => $this->_db_errno, 2821 'error_msg' => $this->_db_errmsg, 2822 'qry' => $this->_db_qry, 2823 'url' => e_REQUEST_URI, 2824 ); 2825 2826 $this->addMessageError('SQL Update Error', $session_messages, $data); //TODO - Lan 2827 $this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$sql->getLastErrorText()); 2828 return false; 2829 } 2830 2831 if($force === false) 2832 { 2833 $this->addMessageInfo(LAN_NO_CHANGE); 2834 } 2835 else 2836 { 2837 $this->addMessageDebug(LAN_NO_CHANGE); 2838 } 2839 2840 2841 return 0; 2842 } 2843 $this->clearCache()->addMessageSuccess(LAN_UPDATED. " #".$this->getId()); 2844 2845 e107::getAdminLog()->addSuccess('TABLE: '.$table, false); 2846 e107::getAdminLog()->addSuccess('WHERE: '.$qry['WHERE'], false); 2847 e107::getAdminLog()->save('ADMINUI_02'); 2848 2849 2850 return $res; 2851 } 2852 2853 /** 2854 * Save data to DB 2855 * 2856 * @param boolen $from_post 2857 * @return boolean|integer 2858 */ 2859 public function save($from_post = true, $force = false, $session_messages = false) 2860 { 2861 if(!$this->getFieldIdName()) 2862 { 2863 return false; 2864 } 2865 2866 if($from_post) 2867 { 2868 //no strict copy, validate & sanitize 2869 $this->mergePostedData(false, true, true); 2870 } 2871 2872 if($this->getId()) 2873 { 2874 return $this->dbUpdate($force, $session_messages); 2875 } 2876 2877 return false; 2878 } 2879 2880 /** 2881 * Update record 2882 * @see save() 2883 * @param boolen $from_post 2884 * @return boolean|integer 2885 *//* 2886 public function update($from_post = true, $force = false, $session_messages = false) 2887 { 2888 if(!$this->getFieldIdName()) 2889 { 2890 return false; 2891 } 2892 2893 if($from_post) 2894 { 2895 //no strict copy, validate & sanitize 2896 $this->mergePostedData(false, true, true); 2897 } 2898 2899 return $this->dbUpdate($force, $session_messages); 2900 }*/ 2901 2902 /** 2903 * Exactly what it says - your debug helper 2904 * @param boolean $retrun 2905 * @param boolean $undo 2906 * @return void 2907 */ 2908 public function saveDebug($return = false, $undo = true) 2909 { 2910 $ret = array(); 2911 2912 $ret['validation_rules'] = $this->getValidationRules(); 2913 $ret['optional_validation_rules'] = $this->getOptionalRules(); 2914 $ret['model_base_ismodfied'] = $this->isModified(); 2915 $ret['model_base_data'] = $this->getData(); 2916 $ret['posted_data'] = $this->getPostedData(); 2917 2918 $this->mergePostedData(false, true, true); 2919 2920 $ret['model_modified_data'] = $this->getData(); 2921 $ret['model_modified_ismodfied'] = $this->isModified(); 2922 $ret['validator_valid_data'] = $this->getValidator()->getValidData(); 2923 2924 // undo 2925 if($undo) 2926 { 2927 $this->setData($ret['model_base_data']) 2928 ->isModified($ret['model_base_ismodfied']) 2929 ->setPostedData($ret['posted_data']); 2930 } 2931 if($return) return $ret; 2932 2933 print_a($ret); 2934 } 2935} 2936 2937//FIXME - move e_model_admin to e_model_admin.php 2938 2939/** 2940 * Base e107 Admin Model class 2941 * 2942 * @package e107 2943 * @category e107_handlers 2944 * @version $Id$ 2945 * @author SecretR 2946 * @copyright Copyright (C) 2008-2010 e107 Inc. 2947 */ 2948class e_admin_model extends e_front_model 2949{ 2950 /** 2951 * Save data to DB 2952 * 2953 * @param boolen $from_post 2954 */ 2955 public function save($from_post = true, $force = false, $session_messages = false) 2956 { 2957 if(!$this->getFieldIdName()) 2958 { 2959 return false; 2960 } 2961 2962 if($from_post) 2963 { 2964 //no strict copy, validate & sanitize 2965 $this->mergePostedData(false, true, true); 2966 } 2967 2968 if($this->getId() && $this->getPostedData('etrigger_submit') !='create') // Additional Check to allow primary ID to be manually set when auto-increment PID is not used. @see userclass2.php 2969 { 2970 return $this->dbUpdate($force, $session_messages); 2971 } 2972 2973 return $this->dbInsert($session_messages); 2974 } 2975 2976 /** 2977 * Insert record 2978 * 2979 * @param boolen $from_post 2980 * @param boolean $session_messages 2981 * @return integer inserted ID or false on error 2982 */ 2983 public function insert($from_post = true, $session_messages = false) 2984 { 2985 if($from_post) 2986 { 2987 //no strict copy, validate & sanitize 2988 $this->mergePostedData(false, true, true); 2989 } 2990 2991 return $this->dbInsert($session_messages); 2992 } 2993 2994 public function delete($ids, $destroy = true, $session_messages = false) 2995 { 2996 $ret = $this->dbDelete(); 2997 if($ret) 2998 { 2999 if($destroy) 3000 { 3001 $this->setMessages($session_messages)->destroy(); 3002 } 3003 } 3004 return $ret; 3005 } 3006 3007 /** 3008 * Insert data to DB 3009 * @param boolean $session_messages to use or not session to store system messages 3010 * @return integer 3011 */ 3012 protected function dbInsert($session_messages = false) 3013 { 3014 $this->_db_errno = 0; 3015 $this->_db_errmsg = ''; 3016 $this->_db_qry = ''; 3017 if($this->hasError()/* || (!$this->data_has_changed && !$force)*/) // not appropriate here! 3018 { 3019 return false; 3020 } 3021 $sql = e107::getDb(); 3022 $sqlQry = $this->toSqlQuery('create'); 3023 $table = $this->getModelTable(); 3024 3025 $res = $sql->insert($table, $sqlQry, $this->getParam('db_debug', false)); 3026 $this->_db_qry = $sql->getLastQuery(); 3027 if(!$res) 3028 { 3029 $this->_db_errno = $sql->getLastErrorNumber(); 3030 $this->_db_errmsg = $sql->getLastErrorText(); 3031 3032 $logData = ($table != 'admin_log') ? array('TABLE'=>$table, 'ERROR'=>$this->_db_errmsg, 'QRY'=>print_r($sqlQry,true)) : false; 3033 3034 $this->addMessageError('SQL Insert Error', $session_messages, $logData); //TODO - Lan 3035 $this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$this->_db_errmsg); 3036 $this->addMessageDebug('SQL QRY Error '.print_a($sqlQry,true)); 3037 3038 return false; 3039 } 3040 3041 e107::getAdminLog()->addSuccess('TABLE: '.$table, false); 3042 e107::getAdminLog()->save('ADMINUI_01'); 3043 // e107::getAdminLog()->clear()->addSuccess($table,false)->addArray($sqlQry)->save('ADMINUI_01'); 3044 3045 // Set the reutrned ID 3046 $this->setId($res); 3047 $this->clearCache()->addMessageSuccess(LAN_CREATED. " #".$this->getId()); 3048 3049 return $res; 3050 } 3051 3052 /** 3053 * Replace data in DB 3054 * 3055 * @param boolean $force force query even if $data_has_changed is false 3056 * @param boolean $session_messages to use or not session to store system messages 3057 */ 3058 protected function dbReplace($force = false, $session_messages = false) 3059 { 3060 $this->_db_errno = 0; 3061 $this->_db_errmsg = ''; 3062 $this->_db_qry = ''; 3063 3064 if($this->hasError()) return false; 3065 if(!$this->data_has_changed && !$force) 3066 { 3067 return 0; 3068 } 3069 $sql = e107::getDb(); 3070 $table = $this->getModelTable(); 3071 $res = $sql->db_Insert($table, $this->toSqlQuery('replace')); 3072 $this->_db_qry = $sql->getLastQuery(); 3073 if(!$res) 3074 { 3075 $this->_db_errno = $sql->getLastErrorNumber(); 3076 $this->_db_errmsg = $sql->getLastErrorText(); 3077 3078 if($this->_db_errno) 3079 { 3080 $logData = ($table != 'admin_log') ? array('TABLE'=>$table, 'ERROR'=>$this->_db_errmsg, 'QRY'=> print_r($this->_db_qry,true)) : false; 3081 3082 $this->addMessageError('SQL Replace Error', $session_messages, $logData); //TODO - Lan 3083 $this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$sql->getLastErrorText()); 3084 } 3085 } 3086 else 3087 { 3088 $this->clearCache(); 3089 } 3090 return $res; 3091 } 3092 3093 /** 3094 * Delete DB data 3095 * 3096 * @param boolean $force force query even if $data_has_changed is false 3097 * @param boolean $session_messages to use or not session to store system messages 3098 */ 3099 protected function dbDelete($session_messages = false) 3100 { 3101 $this->_db_errno = 0; 3102 $this->_db_errmsg = ''; 3103 $this->_db_qry = ''; 3104 3105 if($this->hasError()) 3106 { 3107 return false; 3108 } 3109 3110 if(!$this->getId()) 3111 { 3112 $this->addMessageError('Record not found', $session_messages); //TODO - Lan 3113 return 0; 3114 } 3115 $sql = e107::getDb(); 3116 $id = $this->getId(); 3117 if(is_numeric($id)) $id = intval($id); 3118 else $id = "'".e107::getParser()->toDB($id)."'"; 3119 $table = $this->getModelTable(); 3120 $where = $this->getFieldIdName().'='.$id; 3121 $res = $sql->delete($table, $where); 3122 $this->_db_qry = $sql->getLastQuery(); 3123 3124 if(!$res) 3125 { 3126 $this->_db_errno = $sql->getLastErrorNumber(); 3127 $this->_db_errmsg = $sql->getLastErrorText(); 3128 3129 if($this->_db_errno) 3130 { 3131 $logData = ($table != 'admin_log') ? array('TABLE'=>$table, 'ERROR'=>$this->_db_errmsg, 'WHERE'=>$where) : false; 3132 3133 $this->addMessageError('SQL Delete Error', $session_messages, $logData); //TODO - Lan 3134 $this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$sql->getLastErrorText()); 3135 } 3136 } 3137 else 3138 { 3139 if($table != 'admin_log') 3140 { 3141 $logData = array('TABLE'=>$table, 'WHERE'=>$where); 3142 e107::getAdminLog()->addSuccess($table,false); 3143 e107::getAdminLog()->addArray($logData)->save('ADMINUI_03'); 3144 } 3145 3146 $this->clearCache(); 3147 } 3148 return $res; 3149 } 3150} 3151 3152/** 3153 * Model collection handler 3154 */ 3155class e_tree_model extends e_front_model 3156{ 3157 /** 3158 * Current model DB table, used in all db calls 3159 * This can/should be overwritten by extending the class 3160 * 3161 * @var string 3162 */ 3163 protected $_db_table; 3164 3165 /** 3166 * All records (no limit) cache 3167 * 3168 * @var string 3169 */ 3170 protected $_total = false; 3171 3172 /** 3173 * Constructor 3174 * 3175 */ 3176 function __construct($tree_data = array()) 3177 { 3178 if($tree_data) 3179 { 3180 $this->setTree($tree_data); 3181 } 3182 } 3183 3184 public function getTotal() 3185 { 3186 return $this->_total; 3187 } 3188 3189 public function setTotal($num) 3190 { 3191 $this->_total = $num; 3192 return $this; 3193 } 3194 3195 /** 3196 * Set table name 3197 * @param string $table 3198 * @return e_tree_model 3199 */ 3200 public function setModelTable($table) 3201 { 3202 $this->_db_table = $table; 3203 return $this; 3204 } 3205 3206 /** 3207 * Get table name 3208 * @return string 3209 */ 3210 public function getModelTable() 3211 { 3212 return $this->_db_table; 3213 } 3214 3215 /** 3216 * Get array of models 3217 * @return array 3218 */ 3219 function getTree() 3220 { 3221 return $this->get('__tree', array()); 3222 } 3223 3224 /** 3225 * Set array of models 3226 * @return e_tree_model 3227 */ 3228 function setTree($tree_data, $force = false) 3229 { 3230 if($force || !$this->isTree()) 3231 { 3232 $this->set('__tree', $tree_data); 3233 } 3234 3235 return $this; 3236 } 3237 3238 /** 3239 * Unset all current data 3240 * @return e_tree_model 3241 */ 3242 function unsetTree() 3243 { 3244 $this->remove('__tree'); 3245 return $this; 3246 } 3247 3248 public function isCacheEnabled($checkId = true) 3249 { 3250 return (null !== $this->getCacheString()); 3251 } 3252 3253 public function getCacheString($replace = false) 3254 { 3255 return $this->_cache_string; 3256 } 3257 3258 public function setCacheString($str = null) 3259 { 3260 if(isset($str)) 3261 return parent::setCacheString($str); 3262 3263 if($this->isCacheEnabled() && !$this->getParam('noCacheStringModify')) 3264 { 3265 $str = !$this->getParam('db_query') 3266 ? 3267 $this->getModelTable() 3268 .$this->getParam('nocount') 3269 .$this->getParam('db_where') 3270 .$this->getParam('db_order') 3271 .$this->getParam('db_limit') 3272 : 3273 $this->getParam('db_query'); 3274 3275 return $this->setCacheString($this->getCacheString().'_'.md5($str)); 3276 } 3277 3278 return parent::setCacheString($str); 3279 } 3280 3281 protected function _setCacheData() 3282 { 3283 if(!$this->isCacheEnabled()) 3284 { 3285 return $this; 3286 } 3287 3288 e107::getCache()->set_sys( 3289 $this->getCacheString(true), 3290 $this->toString(false, null, $this->getParam('nocount') ? false : true), 3291 $this->_cache_force, 3292 false 3293 ); 3294 return $this; 3295 } 3296 3297 protected function _loadFromArray($array) 3298 { 3299 if(isset($array['total'])) 3300 { 3301 $this->setTotal((integer) $array['total']); 3302 unset($array['total']); 3303 } 3304 $class_name = $this->getParam('model_class', 'e_model'); 3305 $tree = array(); 3306 foreach ($array as $id => $data) 3307 { 3308 $tree[$id] = new $class_name($data); 3309 $this->_onLoad($tree[$id]); 3310 } 3311 3312 $this->setTree($tree, true); 3313 } 3314 3315 /** 3316 * Additional on load logic to be set from subclasses 3317 * 3318 * @param e_model $node 3319 * @return e_tree_model 3320 */ 3321 protected function _onLoad($node) 3322 { 3323 return $this; 3324 } 3325 3326 /** 3327 * Default load method 3328 * 3329 * @return e_tree_model 3330 */ 3331 public function loadBatch($force = false) 3332 { 3333 if ($force) 3334 { 3335 $this->unsetTree() 3336 ->_clearCacheData(); 3337 3338 $this->_total = false; 3339 } 3340 3341 // XXX What would break if changed to the most proper isTree()? 3342 elseif($this->isTree()) //!$this->isEmpty() 3343 { 3344 return $this; 3345 } 3346 3347 $this->setCacheString(); 3348 $cached = $this->_getCacheData(); 3349 if($cached !== false) 3350 { 3351 $this->_loadFromArray($cached); 3352 return $this; 3353 } 3354 3355 // auto-load all 3356 if(!$this->getParam('db_query') && $this->getModelTable()) 3357 { 3358 $this->setParam('db_query', 'SELECT'.(!$this->getParam('nocount') ? ' SQL_CALC_FOUND_ROWS' : '') 3359 .($this->getParam('db_cols') ? ' '.$this->getParam('db_cols') : ' *').' FROM #'.$this->getModelTable() 3360 .($this->getParam('db_joins') ? ' '.$this->getParam('db_joins') : '') 3361 .($this->getParam('db_where') ? ' WHERE '.$this->getParam('db_where') : '') 3362 .($this->getParam('db_order') ? ' ORDER BY '.$this->getParam('db_order') : '') 3363 .($this->getParam('db_limit') ? ' LIMIT '.$this->getParam('db_limit') : '') 3364 ); 3365 } 3366 3367 $class_name = $this->getParam('model_class', 'e_model'); 3368 if($this->getParam('db_query') && $class_name && class_exists($class_name)) 3369 { 3370 $sql = e107::getDb($this->getParam('model_class', 'e_model')); 3371 $this->_total = $sql->total_results = false; 3372 3373 if($rows = $this->getRows($sql)) 3374 { 3375 foreach($rows as $tmp) 3376 { 3377 $tmp = new $class_name($tmp); 3378 if($this->getParam('model_message_stack')) 3379 { 3380 $tmp->setMessageStackName($this->getParam('model_message_stack')); 3381 } 3382 $this->_onLoad($tmp)->setNode($tmp->get($this->getFieldIdName()), $tmp); 3383 } 3384 unset($tmp); 3385 3386 $this->countResults($sql); 3387 } 3388 3389 if($sql->getLastErrorNumber()) 3390 { 3391 3392 $data = array( 3393 'TABLE' => $this->getModelTable(), 3394 'error_no' => $sql->getLastErrorNumber(), 3395 'error_msg' => $sql->getLastErrorText(), 3396 'qry' => $sql->getLastQuery(), 3397 'url' => e_REQUEST_URI, 3398 ); 3399 3400 3401 $this->addMessageError('Application Error - DB query failed.', false, $data) // TODO LAN 3402 ->addMessageDebug('SQL Error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText()) 3403 ->addMessageDebug($sql->getLastQuery()); 3404 } 3405 else 3406 { 3407 $this->_setCacheData(); 3408 } 3409 3410 } 3411 return $this; 3412 } 3413 3414 protected function getRows($sql) 3415 { 3416 // Tree (Parent-Child Relationship) 3417 if ($this->getParam('sort_parent') && $this->getParam('sort_field')) 3418 { 3419 return $this->getRowsTree($sql); 3420 } 3421 // Flat List 3422 return $this->getRowsList($sql); 3423 } 3424 3425 protected function getRowsList($sql) 3426 { 3427 $success = $sql->gen($this->getParam('db_query'), $this->getParam('db_debug') ? true : false); 3428 if (!$success) return false; 3429 3430 return $sql->rows(); 3431 } 3432 3433 protected function getRowsTree($sql) 3434 { 3435 // Workaround: Parse and modify db_query param for simulated pagination 3436 $this->prepareSimulatedPagination(); 3437 // Workaround: Parse and modify db_query param for simulated custom ordering 3438 $this->prepareSimulatedCustomOrdering(); 3439 3440 $success = $sql->gen($this->getParam('db_query'), $this->getParam('db_debug') ? true : false); 3441 if (!$success) return false; 3442 3443 $rows_tree = self::arrayToTree($sql->rows(), 3444 $this->getParam('primary_field'), 3445 $this->getParam('sort_parent')); 3446 $rows = self::flattenTree($rows_tree, 3447 $this->getParam('sort_field'), 3448 $this->getParam('sort_order')); 3449 3450 // Simulated pagination 3451 $rows = array_splice($rows, 3452 (int) $this->getParam('db_limit_offset'), 3453 ($this->getParam('db_limit_count') ? $this->getParam('db_limit_count') : count($rows)) 3454 ); 3455 3456 return $rows; 3457 } 3458 3459 /** 3460 * Converts a relational array with a parent field and a sort order field to a tree 3461 * @param array $rows Relational array with a parent field and a sort order field 3462 * @param string $primary_field The field name of the primary key (matches children to parents) 3463 * @param string $sort_parent The field name whose value is the parent ID 3464 * @return array Multidimensional array with child nodes under the "_children" key 3465 */ 3466 protected static function arrayToTree($rows, $primary_field, $sort_parent) 3467 { 3468 $nodes = array(); 3469 $root = array($primary_field => 0); 3470 $nodes[] = &$root; 3471 3472 while(!empty($nodes)) 3473 { 3474 self::moveRowsToTreeNodes($nodes, $rows, $primary_field, $sort_parent); 3475 } 3476 3477 return array(0 => $root); 3478 } 3479 3480 /** 3481 * Put rows with parent matching the ID of the first node into the next node's children 3482 * @param array &$nodes Current queue of nodes, the first of which may have children added to it 3483 * @param array &rows The remaining rows that have yet to be converted into children of nodes 3484 * @param string $primary_field The field name of the primary key (matches children to parents) 3485 * @param string $sort_parent The field name whose value is the parent ID 3486 * @returns null 3487 */ 3488 protected static function moveRowsToTreeNodes(&$nodes, &$rows, $primary_field, $sort_parent) 3489 { 3490 $node = &$nodes[0]; 3491 array_shift($nodes); 3492 $nodeID = (int) $node[$primary_field]; 3493 foreach($rows as $key => $row) 3494 { 3495 $rowParentID = (int) $row[$sort_parent]; 3496 3497 // Note: This optimization only works if the SQL query executed was ordered by the sort parent. 3498 if($rowParentID > $nodeID) break; 3499 3500 $node['_children'][] = &$row; 3501 unset($rows[$key]); 3502 $nodes[] = &$row; 3503 unset($row); 3504 } 3505 } 3506 3507 /** 3508 * Flattens a tree into a depth-first array, sorting each node by a field's values 3509 * @param array $tree Tree with child nodes under the "_children" key 3510 * @param mixed $sort_field The field name (string) or field names (array) whose value 3511 * is or values are the sort order in the current tree node 3512 * @param int $sort_order Desired sorting direction: 1 if ascending, -1 if descending 3513 * @param int $depth The depth that this level of recursion is entering 3514 * @return array One-dimensional array in depth-first order with depth indicated by the "_depth" key 3515 */ 3516 protected static function flattenTree($tree, $sort_field = null, $sort_order = 1, $depth = 0) 3517 { 3518 $flat = array(); 3519 3520 foreach($tree as $item) 3521 { 3522 $children = isset($item['_children']) ? $item['_children'] : null; 3523 unset($item['_children']); 3524 $item['_depth'] = $depth; 3525 if($depth > 0) 3526 $flat[] = $item; 3527 if(is_array($children)) 3528 { 3529 uasort($children, function($node1, $node2) use ($sort_field, $sort_order) 3530 { 3531 return self::multiFieldCmp($node1, $node2, $sort_field, $sort_order); 3532 }); 3533 $flat = array_merge($flat, self::flattenTree($children, $sort_field, $sort_order, $depth+1)); 3534 } 3535 } 3536 3537 return $flat; 3538 } 3539 3540 /** 3541 * Naturally compares two associative arrays given multiple sort keys and a reverse order flag 3542 * @param array $row1 Associative array to compare to $row2 3543 * @param array $row2 Associative array to compare to $row1 3544 * @param mixed $sort_field Key (string) or keys (array) to compare 3545 * the values of in both $row1 and $row2 3546 * @param int $sort_order -1 to reverse the sorting order or 1 to keep the order as ascending 3547 * @return int -1 if $row1 is less than $row2 3548 * 0 if $row1 is equal to $row2 3549 * 1 if $row1 is greater than $row2 3550 */ 3551 protected static function multiFieldCmp($row1, $row2, $sort_field, $sort_order = 1) 3552 { 3553 if (!is_array($sort_field)) 3554 $sort_field = [$sort_field]; 3555 $field = array_shift($sort_field); 3556 3557 $cmp = strnatcmp((string) $row1[$field], (string) $row2[$field]); 3558 if ($sort_order === -1 || $sort_order === 1) $cmp *= $sort_order; 3559 if ($cmp === 0 && count($sort_field) >= 1) 3560 return self::multiFieldCmp($row1, $row2, $sort_field, $sort_order); 3561 return $cmp; 3562 } 3563 3564 /** 3565 * Resiliently counts the results from the last SQL query in the given resource 3566 * 3567 * Sets the count in $this->_total 3568 * 3569 * @param resource $sql SQL resource that executed a query 3570 * @return int Number of results from the latest query 3571 */ 3572 protected function countResults($sql) 3573 { 3574 $this->_total = is_integer($sql->total_results) ? $sql->total_results : false; //requires SQL_CALC_FOUND_ROWS in query - see db handler 3575 if(false === $this->_total && $this->getModelTable() && !$this->getParam('nocount')) 3576 { 3577 //SQL_CALC_FOUND_ROWS not found in the query, do one more query 3578 // $this->_total = e107::getDb()->db_Count($this->getModelTable()); // fails with specific listQry 3579 3580 // Calculates correct total when using filters and search. //XXX Optimize. 3581 $countQry = preg_replace('/(LIMIT ([\d,\s])*)$/', "", $this->getParam('db_query')); 3582 3583 $this->_total = e107::getDb()->gen($countQry); 3584 3585 } 3586 return $this->_total; 3587 } 3588 3589 /** 3590 * Workaround: Parse and modify query to prepare for simulation of tree pagination 3591 * 3592 * This is a hack to maintain compatibility of pagination of tree 3593 * models without SQL LIMITs 3594 * 3595 * Implemented out of necessity under 3596 * https://github.com/e107inc/e107/issues/3015 3597 * 3598 * @returns null 3599 */ 3600 protected function prepareSimulatedPagination() 3601 { 3602 $db_query = $this->getParam('db_query'); 3603 $db_query = preg_replace_callback("/LIMIT ([\d]+)[ ]*(?:,|OFFSET){0,1}[ ]*([\d]*)/i", function($matches) 3604 { 3605 // Count only 3606 if (empty($matches[2])) 3607 { 3608 $this->setParam('db_limit_count', $matches[1]); 3609 } 3610 // Offset and count 3611 else 3612 { 3613 $this->setParam('db_limit_offset', $matches[1]); 3614 $this->setParam('db_limit_count', $matches[2]); 3615 } 3616 3617 return ""; 3618 }, $db_query); 3619 $this->setParam('db_query', $db_query); 3620 } 3621 3622 /** 3623 * Workaround: Parse and modify query to prepare for simulation of custom ordering 3624 * 3625 * XXX: Not compliant with all forms of ORDER BY clauses 3626 * XXX: Does not support quoted identifiers (`identifier`) 3627 * XXX: Does not support mixed sort orders (identifier1 ASC, identifier2 DESC) 3628 * 3629 * This is a hack to enable custom ordering of tree models when 3630 * flattening the tree. 3631 * 3632 * Implemented out of necessity under 3633 * https://github.com/e107inc/e107/issues/3029 3634 * 3635 * @returns null 3636 */ 3637 protected function prepareSimulatedCustomOrdering() 3638 { 3639 $db_query = $this->getParam('db_query'); 3640 $db_query = preg_replace_callback('/ORDER BY (?:.+\.)*[\.]*([A-Za-z0-9$_,]+)[ ]*(ASC|DESC)*/i', function($matches) 3641 { 3642 if (!empty($matches[1])) 3643 { 3644 $current_sort_field = $this->getParam('sort_field'); 3645 if (!empty($current_sort_field)) 3646 { 3647 $matches[1] = $current_sort_field.",".$matches[1]; 3648 } 3649 $this->setParam('sort_field', array_map('trim', explode(',', $matches[1]))); 3650 } 3651 if (!empty($matches[2])) 3652 $this->setParam('sort_order', 3653 (0 === strcasecmp($matches[2], 'DESC') ? -1 : 1) 3654 ); 3655 3656 return ""; 3657 }, $db_query) 3658 // Optimization goes with e_tree_model::moveRowsToTreeNodes() 3659 . " ORDER BY " . $this->getParam('sort_parent') . "," . $this->getParam('primary_field'); 3660 $this->setParam('db_query', $db_query); 3661 } 3662 3663 /** 3664 * Get single model instance from the collection 3665 * @param integer $node_id 3666 * @return e_model 3667 */ 3668 function getNode($node_id) 3669 { 3670 return $this->getData('__tree/'.$node_id); 3671 } 3672 3673 /** 3674 * Add or remove (when $node is null) model to the collection 3675 * 3676 * @param integer $node_id 3677 * @param e_model $node 3678 * @return e_tree_model 3679 */ 3680 function setNode($node_id, $node) 3681 { 3682 if(null === $node) 3683 { 3684 $this->removeData('__tree/'.$node_id); 3685 return $this; 3686 } 3687 3688 $this->setData('__tree/'.$node_id, $node); 3689 return $this; 3690 } 3691 3692 /** 3693 * Check if model with passed id exists in the collection 3694 * 3695 * @param integer $node_id 3696 * @return boolean 3697 */ 3698 public function isNode($node_id) 3699 { 3700 return $this->isData('__tree/'.$node_id); 3701 } 3702 3703 /** 3704 * Check if model with passed id exists in the collection and is not empty 3705 * 3706 * @param integer $node_id 3707 * @return boolean 3708 */ 3709 public function hasNode($node_id) 3710 { 3711 return $this->hasData('__tree/'.$node_id); 3712 } 3713 3714 /** 3715 * Check if collection is empty 3716 * 3717 * @return boolean 3718 */ 3719 function isEmpty() 3720 { 3721 return (!$this->has('__tree')); 3722 } 3723 3724 /** 3725 * Check if collection is loaded (not null) 3726 * 3727 * @return boolean 3728 */ 3729 function isTree() 3730 { 3731 return $this->is('__tree'); 3732 } 3733 3734 /** 3735 * Same as isEmpty(), but with opposite boolean logic 3736 * 3737 * @return boolean 3738 */ 3739 function hasTree() 3740 { 3741 return $this->has('__tree'); 3742 } 3743 3744 /** 3745 * Render model data, all 'sc_*' methods will be recongnized 3746 * as shortcodes. 3747 * 3748 * @param string $template 3749 * @param boolean $parsesc parse external shortcodes, default is true 3750 * @param e_vars $eVars simple parser data 3751 * @return string parsed template 3752 */ 3753 public function toHTML($template, $parsesc = true, $eVars = null) 3754 { 3755 $ret = ''; 3756 $i = 1; 3757 foreach ($this->getTree() as $model) 3758 { 3759 if($eVars) $eVars->treeCounter = $i; 3760 $ret .= $model->toHTML($template, $parsesc, $eVars); 3761 $i++; 3762 } 3763 return $ret; 3764 } 3765 3766 public function toXML() 3767 { 3768 return ''; 3769 // UNDER CONSTRUCTION 3770 } 3771 3772 /** 3773 * Convert model object to array 3774 * @param boolean $total include total results property 3775 * @return array object data 3776 */ 3777 public function toArray($total = false) 3778 { 3779 $ret = array(); 3780 foreach ($this->getTree() as $id => $model) 3781 { 3782 $ret[$id] = $model->toArray(); 3783 } 3784 if($total) $ret['total'] = $this->getTotal(); 3785 3786 return $ret; 3787 } 3788 3789 /** 3790 * Convert object data to a string 3791 * 3792 * @param boolean $AddSlashes 3793 * @param string $node_id optional, if set method will return corresponding value as a string 3794 * @param boolean $total include total results property 3795 * @return string 3796 */ 3797 public function toString($AddSlashes = true, $node_id = null, $total = false) 3798 { 3799 if (null !== $node_id && $this->isNode($node_id)) 3800 { 3801 return $this->getNode($node_id)->toString($AddSlashes); 3802 } 3803 return (string) e107::getArrayStorage()->WriteArray($this->toArray($total), $AddSlashes); 3804 } 3805 3806 public function update($from_post = true, $force = false, $session_messages = false) 3807 { 3808 } 3809 3810 public function delete($ids, $destroy = true, $session_messages = false) 3811 { 3812 } 3813} 3814 3815class e_front_tree_model extends e_tree_model 3816{ 3817 /** 3818 * @var integer Last SQL error number 3819 */ 3820 protected $_db_errno = 0; 3821 3822 /** 3823 * @var string Last SQL error message 3824 */ 3825 protected $_db_errmsg = ''; 3826 3827 /** 3828 * @var string Last SQL query 3829 */ 3830 protected $_db_qry = ''; 3831 3832 /** 3833 * @return boolean 3834 */ 3835 public function hasSqlError() 3836 { 3837 return !empty($this->_db_errno); 3838 } 3839 3840 /** 3841 * @return integer last mysql error number 3842 */ 3843 public function getSqlErrorNumber() 3844 { 3845 return $this->_db_errno; 3846 } 3847 3848 /** 3849 * @return string last mysql error message 3850 */ 3851 public function getSqlError() 3852 { 3853 return $this->_db_errmsg; 3854 } 3855 3856 /** 3857 * @return string last mysql error message 3858 */ 3859 public function getSqlQuery() 3860 { 3861 return $this->_db_qry; 3862 } 3863 3864 /** 3865 * @return boolean 3866 */ 3867 public function hasError() 3868 { 3869 return $this->hasSqlError(); 3870 } 3871 3872 /** 3873 * Batch update tree records/nodes 3874 * @param string $field field name 3875 * @param string $value 3876 * @param string|array $ids numerical array or string comma separated ids 3877 * @param mixed $syncvalue value to be used for model data synchronization (db value could be something like '1-field_name'), null - no sync 3878 * @param boolean $sanitize [optional] default true 3879 * @param boolean $session_messages [optional] default false 3880 * @return integer updated count or false on error 3881 */ 3882 public function batchUpdate($field, $value, $ids, $syncvalue = null, $sanitize = true, $session_messages = false) 3883 { 3884 $tp = e107::getParser(); 3885 $sql = e107::getDb(); 3886 if(empty($ids)) 3887 { 3888 return 0; 3889 } 3890 if(!is_array($ids)) 3891 { 3892 $ids = explode(',', $ids); 3893 } 3894 3895 if(true === $syncvalue) 3896 { 3897 $syncvalue = $value; 3898 } 3899 3900 if($sanitize) 3901 { 3902 $ids = array_map(array($tp, 'toDB'), $ids); 3903 $field = $tp->toDB($field); 3904 $value = "'".$tp->toDB($value)."'"; 3905 } 3906 $idstr = implode(', ', $ids); 3907 3908 $table = $this->getModelTable(); 3909 3910 $res = $sql->update($table, "{$field}={$value} WHERE ".$this->getFieldIdName().' IN ('.$idstr.')', $this->getParam('db_debug', false)); 3911 $this->_db_errno = $sql->getLastErrorNumber(); 3912 $this->_db_errmsg = $sql->getLastErrorText(); 3913 $this->_db_qry = $sql->getLastQuery(); 3914 3915 if(!$res) 3916 { 3917 if($sql->getLastErrorNumber()) 3918 { 3919 $data = array( 3920 'TABLE' => $table , 3921 'error_no' => $this->_db_errno, 3922 'error_msg' => $this->_db_errmsg, 3923 'qry' => $this->_db_qry, 3924 'url' => e_REQUEST_URI, 3925 ); 3926 3927 3928 $this->addMessageError(LAN_UPDATED_FAILED, $session_messages, $data); 3929 $this->addMessageDebug('SQL Error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText()); 3930 } 3931 else 3932 { 3933 $this->addMessageInfo(LAN_NO_CHANGE, $session_messages); 3934 } 3935 } 3936 else 3937 { 3938 $this->clearCache(); 3939 } 3940 3941 $modelCacheCheck = $this->getParam('clearModelCache'); 3942 if(null === $syncvalue && !$modelCacheCheck) return $res; 3943 3944 foreach ($ids as $id) 3945 { 3946 $node = $this->getNode($id); 3947 if(!$node) continue; 3948 3949 if(null !== $syncvalue) 3950 { 3951 $node->set($field, $syncvalue) 3952 ->setMessages($session_messages); 3953 } 3954 if($modelCacheCheck) $this->clearCache(); 3955 } 3956 return $res; 3957 } 3958} 3959 3960class e_admin_tree_model extends e_front_tree_model 3961{ 3962 3963 3964 /** 3965 * Batch Delete records 3966 * @param mixed $ids 3967 * @param boolean $destroy [optional] destroy object instance after db delete 3968 * @param boolean $session_messages [optional] 3969 * @return integer deleted records number or false on DB error 3970 */ 3971 public function delete($ids, $destroy = true, $session_messages = false) 3972 { 3973 if(!$ids) return 0; 3974 3975 if(!is_array($ids)) 3976 { 3977 $ids = explode(',', $ids); 3978 } 3979 3980 $tp = e107::getParser(); 3981 $ids = array_map(array($tp, 'toDB'), $ids); 3982 $idstr = implode(', ', $ids); 3983 3984 $sql = e107::getDb(); 3985 $table = $this->getModelTable(); 3986 $sqlQry = $this->getFieldIdName().' IN (\''.$idstr.'\')'; 3987 3988 $res = $sql->delete($table, $sqlQry); 3989 3990 $this->_db_errno = $sql->getLastErrorNumber(); 3991 $this->_db_errmsg = $sql->getLastErrorText(); 3992 $this->_db_qry = $sql->getLastQuery(); 3993 3994 $modelCacheCheck = $this->getParam('clearModelCache'); 3995 3996 if(!$res) 3997 { 3998 if($sql->getLastErrorNumber()) 3999 { 4000 $data = array( 4001 'TABLE' => $table, 4002 'error_no' => $this->_db_errno, 4003 'error_msg' => $this->_db_errmsg, 4004 'qry' => $this->_db_qry, 4005 'url' => e_REQUEST_URI, 4006 ); 4007 4008 4009 $this->addMessageError('SQL Delete Error: ' . $sql->getLastQuery(), $session_messages, $data); //TODO - Lan 4010 $this->addMessageDebug('SQL Error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText()); 4011 } 4012 } 4013 elseif($destroy || $modelCacheCheck) 4014 { 4015 foreach ($ids as $id) 4016 { 4017 if($this->hasNode($id)) 4018 { 4019 $this->getNode($id)->clearCache()->setMessages($session_messages); 4020 if($destroy) 4021 { 4022 call_user_func(array($this->getNode(trim($id)), 'destroy')); // first call model destroy method if any 4023 $this->setNode($id, null); 4024 } 4025 } 4026 } 4027 } 4028 4029 if($table != 'admin_log') 4030 { 4031 $logData = array('TABLE'=>$table, 'WHERE'=>$sqlQry); 4032 e107::getAdminLog()->addArray($logData)->save('ADMINUI_03'); 4033 } 4034 return $res; 4035 } 4036 4037 /** 4038 * Batch Copy Table Rows. 4039 */ 4040 public function copy($ids, $session_messages = false) 4041 { 4042 if(empty($ids[0])) 4043 { 4044 $this->addMessageError('No IDs provided', $session_messages); //TODO - Lan 4045 $this->addMessageDebug(print_a(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS),true),$session_messages); //TODO - Lan 4046 return false; 4047 } 4048 4049 4050 $tp = e107::getParser(); 4051 $ids = array_map(array($tp, 'toDB'), $ids); 4052 $idstr = implode(', ', $ids); 4053 4054 $sql = e107::getDb(); 4055 $res = $sql->db_CopyRow($this->getModelTable(), "*", $this->getFieldIdName().' IN ('.$idstr.')'); 4056 if(false !== $res) 4057 { 4058 $this->addMessageSuccess('Copied #'.$idstr); 4059 } 4060 else 4061 { 4062 if($sql->getLastErrorNumber()) 4063 { 4064 $this->addMessageError('SQL Copy Error', $session_messages); //TODO - Lan 4065 $this->addMessageDebug('SQL Error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText()); 4066 $this->addMessageDebug('$SQL Query'.print_a($sql->getLastQuery(),true)); 4067 } 4068 } 4069 $this->_db_errno = $sql->getLastErrorNumber(); 4070 $this->_db_errmsg = $sql->getLastErrorText(); 4071 $this->_db_qry = $sql->getLastQuery(); 4072 return $res; 4073 } 4074 4075 4076 /** 4077 * Get urls/url data for given nodes 4078 */ 4079 public function url($ids, $options = array(), $extended = false) 4080 { 4081 $ret = array(); 4082 foreach ($ids as $id) 4083 { 4084 if(!$this->hasNode($id)) continue; 4085 4086 $model = $this->getNode($id); 4087 if($this->getUrl()) $model->setUrl($this->getUrl()); // copy url config data if available 4088 $ret[$id] = $model->url(null, $options, $extended); 4089 } 4090 return $ret; 4091 } 4092 4093 4094 /** 4095 * Export Selected Data 4096 * @param $ids 4097 * @return null 4098 */ 4099 public function export($ids) 4100 { 4101 $ids = e107::getParser()->filter($ids,'int'); 4102 4103 if(empty($ids)) 4104 { 4105 return false; 4106 } 4107 4108 $idstr = implode(', ', $ids); 4109 4110 $table = array($this->getModelTable()); 4111 4112 $filename = "e107Export_" .$this->getModelTable()."_". date("YmdHi").".xml"; 4113 $query = $this->getFieldIdName().' IN ('.$idstr.') '; // ORDER BY '.$this->getParam('db_order') ; 4114 4115 e107::getXml()->e107Export(null,$table,null,null, array('file'=>$filename,'query'=>$query)); 4116 4117 return null; 4118 4119 } 4120} 4121