From 5e3b0ea10d5adc525f6af0190a2d5d2df7bc8e6c Mon Sep 17 00:00:00 2001 From: Pathologic Date: Tue, 16 Feb 2021 19:45:37 +0300 Subject: [PATCH] init --- README.md | 9 +- composer.json | 19 + src/APIhelpers.php | 486 ++++++++++++++++++ src/MODxAPI.php | 1014 ++++++++++++++++++++++++++++++++++++++ src/Traits/ContentTV.php | 146 ++++++ src/Traits/RoleTV.php | 38 ++ src/autoTable.php | 138 ++++++ src/modResource.php | 944 +++++++++++++++++++++++++++++++++++ src/modUsers.php | 872 ++++++++++++++++++++++++++++++++ 9 files changed, 3665 insertions(+), 1 deletion(-) create mode 100644 composer.json create mode 100644 src/APIhelpers.php create mode 100644 src/MODxAPI.php create mode 100644 src/Traits/ContentTV.php create mode 100644 src/Traits/RoleTV.php create mode 100644 src/autoTable.php create mode 100644 src/modResource.php create mode 100644 src/modUsers.php diff --git a/README.md b/README.md index ebb5e45..b345dd9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,9 @@ # MODxAPI -legacy libs for Evolution CMS 3.0 +Legacy libs for Evolution CMS 3.0. + +``` +use Pathologic\EvolutionCMS\MODxAPI\modResource; +... +$doc = new modResource($modx); +... +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6ca9cd1 --- /dev/null +++ b/composer.json @@ -0,0 +1,19 @@ +{ + "name": "pathologic/modxapi", + "description": "MODxAPI for Evolution CMS", + "license": "MIT", + "authors": [ + { + "name": "pathologic", + "email": "m@xim.name" + } + ], + "require": { + "pathologic/doctrine-cache-bridge": "*" + }, + "autoload": { + "psr-4": { + "Pathologic\\EvolutionCMS\\MODxAPI\\": "src/" + } + } +} \ No newline at end of file diff --git a/src/APIhelpers.php b/src/APIhelpers.php new file mode 100644 index 0000000..085ace6 --- /dev/null +++ b/src/APIhelpers.php @@ -0,0 +1,486 @@ + + * @version 0.1 + * + * @param string $html HTML текст + * @param integer $len максимальная длина строки + * @param string $encoding кодировка + * @return string + */ + public static function mb_trim_word($html, $len, $encoding = 'UTF-8') + { + $text = trim(preg_replace('|\s+|', ' ', strip_tags($html))); + $text = mb_substr($text, 0, $len + 1, $encoding); + if (mb_substr($text, -1, null, $encoding) == ' ') { + $out = trim($text); + } else { + $out = mb_substr($text, 0, mb_strripos($text, ' ', null, $encoding), $encoding); + } + + return preg_replace("/(([\.,\-:!?;\s])|(&\w+;))+$/ui", "", $out); + } + + /** + * Получение значения по ключу из массива, либо возврат значения по умолчанию + * + * @param mixed $data массив + * @param string $key ключ массива + * @param mixed $default null значение по умолчанию + * @param Closure $validate null функция дополнительной валидации значения (должна возвращать true или false) + * @return mixed + */ + public static function getkey($data, $key, $default = null, $validate = null) + { + $out = $default; + if (is_array($data) && (is_int($key) || is_string($key)) && $key !== '' && array_key_exists($key, $data)) { + $out = $data[$key]; + } + if (! empty($validate) && is_callable($validate)) { + $out = (($validate($out) === true) ? $out : $default); + } + + return $out; + } + + /** + * Email validate + * + * @category validate + * @version 0.1 + * @license GNU General Public License (GPL), http://www.gnu.org/copyleft/gpl.html + * @param string $email проверяемый email + * @param boolean $dns проверять ли DNS записи + * @return boolean|string Результат проверки почтового ящика + * @author Anton Shevchuk + */ + public static function emailValidate($email, $dns = true) + { + if (filter_var($email, FILTER_VALIDATE_EMAIL)) { + list(, $domain) = explode("@", $email, 2); + if (! $dns || ($dns && checkdnsrr($domain, "MX") && checkdnsrr($domain, "A"))) { + $error = false; + } else { + $error = 'dns'; + } + } else { + $error = 'format'; + } + + return $error; + } + + /** + * Password generate + * + * @category generate + * @version 0.1 + * @license GNU General Public License (GPL), http://www.gnu.org/copyleft/gpl.html + * @param string $len длина пароля + * @param string $data правила генерации пароля + * @return string Строка с паролем + * @author Agel_Nash + * + * Расшифровка значений $data + * "A": A-Z буквы + * "a": a-z буквы + * "0": цифры + * ".": все печатные символы + * + * @example + * $this->genPass(10,"Aa"); //nwlTVzFdIt + * $this->genPass(8,"0"); //71813728 + * $this->genPass(11,"A"); //VOLRTMEFAEV + * $this->genPass(5,"a0"); //4hqi7 + * $this->genPass(5,"."); //2_Vt} + * $this->genPass(20,"."); //AMV,>&?J)v55,(^g}Z06 + * $this->genPass(20,"aaa0aaa.A"); //rtvKja5xb0\KpdiRR1if + */ + public static function genPass($len, $data = '') + { + if ($data == '') { + $data = 'Aa0.'; + } + $opt = strlen($data); + $pass = array(); + + for ($i = $len; $i > 0; $i--) { + switch ($data[rand(0, ($opt - 1))]) { + case 'A': + $tmp = rand(65, 90); + break; + case 'a': + $tmp = rand(97, 122); + break; + case '0': + $tmp = rand(48, 57); + break; + default: + $tmp = rand(33, 126); + } + $pass[] = chr($tmp); + } + $pass = implode("", $pass); + + return $pass; + } + + /** + * @param $data + * @return bool|false|string + */ + public static function getEnv($data) + { + switch (true) { + case (isset($_SERVER[$data])): + $out = $_SERVER[$data]; + break; + case (isset($_ENV[$data])): + $out = $_ENV[$data]; + break; + case ($tmp = getenv($data)): + $out = $tmp; + break; + case (function_exists('apache_getenv') && $tmp = apache_getenv($data, true)): + $out = $tmp; + break; + default: + $out = false; + } + unset($tmp); + + return $out; + } + + /** + * User IP + * + * @category validate + * @version 0.1 + * @license GNU General Public License (GPL), http://www.gnu.org/copyleft/gpl.html + * @param string $default IP адрес который будет отдан функцией, если больше ничего не обнаружено + * @return string IP пользователя + * @author Agel_Nash + * + * @see http://stackoverflow.com/questions/5036443/php-how-to-block-proxies-from-my-site + */ + public static function getUserIP($default = '127.0.0.1') + { + //Порядок условий зависит от приоритетов + switch (true) { + case ($tmp = self::getEnv('HTTP_COMING_FROM')): + $out = $tmp; + break; + case ($tmp = self::getEnv('HTTP_X_COMING_FROM')): + $out = $tmp; + break; + case ($tmp = self::getEnv('HTTP_VIA')): + $out = $tmp; + break; + case ($tmp = self::getEnv('HTTP_FORWARDED')): + $out = $tmp; + break; + case ($tmp = self::getEnv('HTTP_FORWARDED_FOR')): + $out = $tmp; + break; + case ($tmp = self::getEnv('HTTP_X_FORWARDED')): + $out = $tmp; + break; + case ($tmp = self::getEnv('HTTP_X_FORWARDED_FOR')): + $out = $tmp; + break; + case (! empty($_SERVER['REMOTE_ADDR'])): + $out = $_SERVER['REMOTE_ADDR']; + break; + default: + $out = false; + } + unset($tmp); + + return (false !== $out && preg_match('|^(?:[0-9]{1,3}\.){3,3}[0-9]{1,3}$|', $out, $matches)) ? $out : $default; + } + + /** + * @param $data + * @param string $charset + * @param array $chars + * @return array|mixed|string + */ + public static function sanitarTag( + $data, + $charset = 'UTF-8', + $chars = array( + '[' => '[', + '%5B' => '[', + ']' => ']', + '%5D' => ']', + '{' => '{', + '%7B' => '{', + '}' => '}', + '%7D' => '}', + '`' => '`', + '%60' => '`' + ) + ) { + switch (true) { + case is_scalar($data): + $out = str_replace( + array_keys($chars), + array_values($chars), + $charset === null ? $data : self::e($data, $charset) + ); + break; + case is_array($data): + $out = array(); + foreach ($data as $key => $val) { + $key = self::sanitarTag($key, $charset, $chars); + $out[$key] = self::sanitarTag($val, $charset, $chars); + } + break; + default: + $out = ''; + } + + return $out; + } + + /** + * @param $text + * @param string $charset + * @return string + */ + public static function e($text, $charset = 'UTF-8') + { + return is_scalar($text) ? htmlspecialchars($text, ENT_QUOTES, $charset, false) : ''; + } + + /** + * Проверка строки на наличе запрещенных символов + * Проверка конечно круто, но валидация русских символов в строке порой завершается не удачей по разным причинам + * (начиная от кривых настроек сервера и заканчивая кривыми настройками кодировки на сайте) + * + * @param string $value Проверяемая строка + * @param int $minLen Минимальная длина строки + * @param array $alph Разрешенные алфавиты + * @param array $mixArray Примесь символов, которые так же могут использоваться в строке + * @return bool + */ + public static function checkString($value, $minLen = 1, $alph = array(), $mixArray = array()) + { + $flag = true; + $len = mb_strlen($value, 'UTF-8'); + $value = trim($value); + if (mb_strlen($value, 'UTF-8') == $len) { + $data = is_array($mixArray) ? $mixArray : array(); + $alph = is_array($alph) ? array_unique($alph) : array(); + foreach ($alph as $item) { + $item = strtolower($item); + switch ($item) { + case 'rus': + $data = array_merge($data, array( + 'А', + 'Б', + 'В', + 'Г', + 'Д', + 'Е', + 'Ё', + 'Ж', + 'З', + 'И', + 'Й', + 'К', + 'Л', + 'М', + 'Н', + 'О', + 'П', + 'Р', + 'С', + 'Т', + 'У', + 'Ф', + 'Х', + 'Ц', + 'Ч', + 'Ш', + 'Щ', + 'Ъ', + 'Ы', + 'Ь', + 'Э', + 'Ю', + 'Я' + )); + break; + case 'num': + $tmp = range('0', '9'); + foreach ($tmp as $t) { + $data[] = (string)$t; + } + break; + case 'eng': + $data = array_merge($data, range('A', 'Z')); + break; + } + } + for ($i = 0; $i < $len; $i++) { + $chr = mb_strtoupper(mb_substr($value, $i, 1, 'UTF-8'), 'UTF-8'); + if (!in_array($chr, $data, true)) { + $flag = false; + break; + } + } + $flag = ($flag && $len >= $minLen); + } else { + $flag = false; + } + + return $flag; + } + + /** + * @param $IDs + * @param string $sep + * @param integer[] $ignore + * @return array + * @throws Exception + */ + public static function cleanIDs($IDs, $sep = ',', $ignore = array()) + { + $out = array(); + if (!is_array($IDs)) { + if (is_scalar($IDs)) { + $IDs = explode($sep, $IDs); + } else { + $IDs = array(); + throw new Exception('Invalid IDs list
' . print_r($IDs, 1) . '
'); + } + } + foreach ($IDs as $item) { + $item = trim($item); + if (is_scalar($item) && (int)$item >= 0) { //Fix 0xfffffffff + if (empty($ignore) || !\in_array((int)$item, $ignore, true)) { + $out[] = (int)$item; + } + } + } + $out = array_unique($out); + + return $out; + } + + /** + * Предварительная обработка данных перед вставкой в SQL запрос вида IN + * Если данные в виде строки, то происходит попытка сформировать массив из этой строки по разделителю $sep + * Точно по тому, по которому потом данные будут собраны обратно + * + * @param integer|string|array $data данные для обработки + * @param string $sep разделитель + * @param boolean $quote заключать ли данные на выходе в кавычки + * @return string обработанная строка + */ + public static function sanitarIn($data, $sep = ',', $quote = true) + { + $modx = evolutionCMS(); + if (is_scalar($data)) { + $data = explode($sep, $data); + } + if (!is_array($data)) { + $data = array(); //@TODO: throw + } + + $out = array(); + foreach ($data as $item) { + if ($item !== '') { + $out[] = $modx->db->escape($item); + } + } + $q = $quote ? "'" : ""; + $out = $q . implode($q . "," . $q, $out) . $q; + + return $out; + } + + /** + * Переменовывание элементов массива + * + * @param array $data массив с данными + * @param string $prefix префикс ключей + * @param string $suffix суффикс ключей + * @param string $addPS разделитель суффиксов, префиксов и ключей массива + * @param string $sep разделитель ключей при склейке многомерных массивов + * @return array массив с переименованными ключами + */ + public static function renameKeyArr($data, $prefix = '', $suffix = '', $addPS = '.', $sep = '.') + { + $out = array(); + if ($prefix == '' && $suffix == '') { + $out = $data; + } else { + $InsertPrefix = ($prefix != '') ? ($prefix . $addPS) : ''; + $InsertSuffix = ($suffix != '') ? ($addPS . $suffix) : ''; + foreach ($data as $key => $item) { + $key = $InsertPrefix . $key; + $val = null; + switch (true) { + case is_scalar($item): + $val = $item; + break; + case is_array($item): + $val = self::renameKeyArr($item, $key . $sep, $InsertSuffix, '', $sep); + $out = array_merge($out, $val); + $val = ''; + break; + } + $out[$key . $InsertSuffix] = $val; + } + } + + return $out; + } +} \ No newline at end of file diff --git a/src/MODxAPI.php b/src/MODxAPI.php new file mode 100644 index 0000000..887f952 --- /dev/null +++ b/src/MODxAPI.php @@ -0,0 +1,1014 @@ +modx = $modx; + $this->setDebug($debug); + $this->_decodedFields = new Collection(); + $this->cache = $this->modx->offsetExists('cache') && $this->modx->cache instanceof Cache; + } + + /** + * @param boolean $flag + * @return $this + */ + public function setDebug($flag) + { + $this->_debug = (bool) $flag; + + return $this; + } + + /** + * @return bool + */ + public function getDebug() + { + return $this->_debug; + } + + /** + * @return array + */ + public function getDefaultFields() + { + return $this->default_field; + } + + /** + * @param $value + * @return int|mixed|string + */ + protected function getTime($value) + { + $value = trim($value); + if (!empty($value)) { + if (!is_numeric($value)) { + $value = (int) strtotime($value); + } + if (!empty($value)) { + $value += $this->modxConfig('server_offset_time'); + } + } + + return $value; + } + + /** + * @param string $name + * @param null $default + * @return mixed + */ + final public function modxConfig($name, $default = null) + { + return APIhelpers::getkey($this->modx->config, $name, $default); + } + + /** + * @param $q + * @return $this + */ + public function addQuery($q) + { + if (is_scalar($q) && !empty($q)) { + $this->_query[] = $q; + } + + return $this; + } + + /** + * @return array + */ + public function getQueryList() + { + return $this->_query; + } + + /** + * @param $SQL + * @return mixed + */ + final public function query($SQL) + { + if ($this->getDebug()) { + $this->addQuery($SQL); + } + + return empty($SQL) ? null : $this->modx->db->query($SQL); + } + + /** + * @param $value + * @return string|void + */ + final public function escape($value) + { + if (!is_scalar($value)) { + $value = ''; + } else { + $value = $this->modx->db->escape($value); + } + + return $value; + } + + /** + * @param string $name + * @param array $data + * @param bool $flag + * @return $this + */ + final public function invokeEvent($name, $data = [], $flag = false) + { + if ((bool) $flag === true) { + $this->modx->invokeEvent($name, $data); + } + + return $this; + } + + /** + * @param string $name + * @param array $data + * @param boolean $flag + * @return array|bool + */ + final public function getInvokeEventResult($name, $data = [], $flag = null) + { + $flag = (isset($flag) && $flag !== '') ? (bool) $flag : false; + + return $flag ? $this->modx->invokeEvent($name, $data) : false; + } + + /** + * @return $this + */ + final public function clearLog() + { + $this->log = []; + + return $this; + } + + /** + * @return array + */ + final public function getLog() + { + return $this->log; + } + + /** + * @param bool $flush + * @return $this + */ + final public function list_log($flush = false) + { + echo '
' . print_r(APIhelpers::sanitarTag($this->log), true) . '
'; + if ($flush) { + $this->clearLog(); + } + + return $this; + } + + /** + * @param bool $full + * @return string + */ + final public function getCachePath($full = true) + { + $path = $this->modx->getCachePath(); + if ($full) { + $path = MODX_BASE_PATH . substr($path, strlen(MODX_BASE_URL)); + } + + return $path; + } + + /** + * @param boolean $fire_events + * @param bool $custom + */ + final public function clearCache($fire_events = false, $custom = false) + { + $IDs = []; + if ($custom === false) { + $this->modx->clearCache(); + include_once(MODX_MANAGER_PATH . 'processors/cache_sync.class.processor.php'); + $sync = new synccache(); + $path = $this->getCachePath(true); + $sync->setCachepath($path); + $sync->setReport(false); + $sync->emptyCache(); + } else { + if (is_scalar($custom)) { + $custom = [$custom]; + } + switch ($this->modx->config['cache_type']) { + case 2: + $cacheFile = "_*.pageCache.php"; + break; + default: + $cacheFile = ".pageCache.php"; + } + if (is_array($custom)) { + foreach ($custom as $id) { + $tmp = glob(MODX_BASE_PATH . "assets/cache/docid_" . $id . $cacheFile); + foreach ($tmp as $file) { + if (is_readable($file)) { + unlink($file); + } + $IDs[] = $id; + } + } + } + clearstatcache(); + } + $this->invokeEvent('OnSiteRefresh', ['IDs' => $IDs], $fire_events); + } + + /** + * @param integer $id + * @return MODxAPI + */ + public function switchObject($id) + { + switch (true) { + //Если загружен другой объект - не тот, с которым мы хотим временно поработать + case ($this->getID() != $id && $id): + $obj = clone $this; + $obj->edit($id); + break; + //Если уже загружен объект, с которым мы хотим временно поработать + case ($this->getID() == $id && $id): + //Если $id не указан, но уже загружен какой-то объект + case (!$id && null !== $this->getID()): + default: + $obj = $this; + break; + } + + return $obj; + } + + /** + * @param bool $flag + * @return $this + */ + public function useIgnore($flag = true) + { + $this->ignoreError = $flag ? 'IGNORE' : ''; + + return $this; + } + + /** + * @return bool + */ + public function hasIgnore() + { + return (bool) $this->ignoreError; + } + + /** + * @param $key + * @param $value + * @return $this + */ + public function set($key, $value) + { + if ((is_scalar($value) || $this->isJsonField($key)) && is_scalar($key) && !empty($key)) { + $this->field[$key] = $value; + } + + return $this; + } + + /** + * @return null|int + */ + final public function getID() + { + return $this->id; + } + + /** + * @param $key + * @return mixed + */ + public function get($key) + { + return APIhelpers::getkey($this->field, $key, null); + } + + /** + * @param $data + * @return $this + */ + public function fromArray($data) + { + if (is_array($data)) { + foreach ($data as $key => $value) { + $this->set($key, $value); + } + } + + return $this; + } + + /** + * Формирует массив значений для подстановки в SQL запрос на обновление + * + * @param $key + * @param string $id + * @return $this + * @throws Exception + */ + final protected function Uset($key, $id = '') + { + if (!isset($this->field[$key])) { + $tmp = "`{$key}`=''"; + $this->log[] = "{$key} is empty"; + } else { + if ($this->issetField($key) && is_scalar($this->field[$key])) { + $tmp = "`{$key}`='{$this->escape($this->field[$key])}'"; + } else { + throw new Exception("{$key} is invalid
" . print_r($this->field[$key], true) . "
"); + } + } + if (!empty($tmp) && $this->isChanged($key)) { + if ($id == '') { + $this->set[] = $tmp; + } else { + $this->set[$id][] = $tmp; + } + } + + return $this; + } + + /** + * Сохраняет начальные значения полей + * + * @param array $data + * @return $this + */ + public function store($data = []) + { + if (is_array($data)) { + $this->store = $data; + } + + return $this; + } + + /** + * Откатывает изменения отдельного поля или всех полей сразу + * + * @param string $key + * @return MODxAPI + */ + public function rollback($key = '') + { + if (!empty($key) && isset($this->store[$key])) { + $this->set($key, $this->store[$key]); + } else { + $this->fromArray($this->store); + } + + return $this; + } + + /** + * Проверяет изменилось ли поле + * + * @param $key + * @return bool + */ + public function isChanged($key) + { + $flag = !isset($this->store[$key]) || (isset($this->store[$key], $this->field[$key]) && $this->store[$key] != (string) $this->field[$key]); + + return $flag; + } + + /** + * @param $IDs + * @param string $sep + * @param integer[] $ignore + * @return array + * @throws Exception + */ + final public function cleanIDs($IDs, $sep = ',', $ignore = []) + { + $out = APIhelpers::cleanIDs($IDs, $sep, $ignore); + + return $out; + } + + /** + * @param $data + * @param null $callback + * @return $this + * @throws Exception + */ + final public function fromJson($data, $callback = null) + { + if (is_scalar($data) && !empty($data)) { + $json = json_decode($data); + } else { + throw new Exception("json is not string with json data"); + } + + if ($this->jsonError($json)) { + if (isset($callback) && is_callable($callback)) { + call_user_func_array($callback, [$json]); + } else { + if (isset($callback)) { + throw new Exception("Can't call callback JSON unpack
" . print_r($callback, 1) . "
"); + } + foreach ($json as $key => $val) { + $this->set($key, $val); + } + } + } else { + throw new Exception('Error from JSON decode:
' . print_r($data, 1) . '
'); + } + + return $this; + } + + /** + * @param null $callback + * @return string + * @throws Exception + */ + final public function toJson($callback = null) + { + $data = $this->toArray(); + if (isset($callback) && is_callable($callback)) { + $data = call_user_func_array($callback, [$data]); + } else { + if (isset($callback)) { + throw new Exception("Can't call callback JSON pre pack
" . print_r($callback, 1) . "
"); + } + } + $json = json_encode($data); + + if ($this->jsonError($data)) { + throw new Exception('Error from JSON decode:
' . print_r($data, 1) . '
'); + } + + return $json; + } + + /** + * @param $data + * @return bool + */ + final protected function jsonError($data) + { + $flag = false; + if (json_last_error() === JSON_ERROR_NONE && is_object($data) && $data instanceof stdClass) { + $flag = true; + } + + return $flag; + } + + /** + * @param string $prefix + * @param string $suffix + * @param string $sep + * @return array + */ + public function toArray($prefix = '', $suffix = '', $sep = '_', $render = false) + { + $tpl = ''; + $plh = '[+key+]'; + if ($prefix !== '') { + $tpl = $prefix . $sep; + } + $tpl .= $plh; + if ($suffix !== '') { + $tpl .= $sep . $suffix; + } + $out = []; + $fields = $this->field; + $fields[$this->fieldPKName()] = $this->getID(); + if ($tpl !== $plh) { + foreach ($fields as $key => $value) { + $out[str_replace($plh, $key, $tpl)] = $value; + } + } else { + $out = $fields; + } + + return $out; + } + + /** + * @return string + */ + final public function fieldPKName() + { + return $this->pkName; + } + + /** + * @param $table + * @return mixed|string + */ + final public function makeTable($table) + { + //Без использования APIHelpers::getkey(). Иначе getFullTableName будет всегда выполняться + return (isset($this->_table[$table])) ? $this->_table[$table] : $this->modx->getFullTableName($table); + } + + /** + * @param $data + * @param string $sep + * @return array|string + */ + final public function sanitarIn($data, $sep = ',') + { + if (!is_array($data)) { + $data = explode($sep, $data); + } + $out = []; + foreach ($data as $item) { + if ($item !== '') { + $out[] = $this->escape($item); + } + } + $out = empty($out) ? '' : "'" . implode("','", $out) . "'"; + + return $out; + } + + /** + * @param string $table + * @param string $field + * @param string $PK + * @return bool + */ + public function checkUnique($table, $field, $PK = 'id') + { + if (is_array($field)) { + $where = []; + foreach ($field as $_field) { + $val = $this->get($_field); + if ($val != '') { + $where[] = "`" . $this->escape($_field) . "` = '" . $this->escape($val) . "'"; + } + } + $where = implode(' AND ', $where); + } else { + $where = ''; + $val = $this->get($field); + if ($val != '') { + $where = "`" . $this->escape($field) . "` = '" . $this->escape($val) . "'"; + } + } + + if ($where != '') { + $sql = $this->query("SELECT `" . $this->escape($PK) . "` FROM " . $this->makeTable($table) . " WHERE " . $where); + $id = $this->modx->db->getValue($sql); + $flag = (!$id || (!$this->newDoc && $id == $this->getID())); + } else { + $flag = false; + } + + return $flag; + } + + /** + * @param array $data + * @return $this + */ + public function create($data = []) + { + $this->close(); + $this->fromArray($data); + + return $this; + } + + /** + * @param $id + * @return $this + */ + public function copy($id) + { + $this->edit($id)->id = 0; + $this->newDoc = true; + $this->store = []; + + return $this; + } + + /** + * + */ + public function close() + { + $this->newDoc = true; + $this->id = null; + $this->field = []; + $this->set = []; + $this->store = []; + $this->markAllDecode(); + } + + /** + * @param $key + * @return bool + */ + public function issetField($key) + { + return (is_scalar($key) && array_key_exists($key, $this->default_field)); + } + + /** + * @param $id + * @return mixed + */ + abstract public function edit($id); + + /** + * @param bool $fire_events + * @param bool $clearCache + * @return mixed + */ + abstract public function save($fire_events = false, $clearCache = false); + + /** + * @param $ids + * @param null $fire_events + * @return mixed + */ + abstract public function delete($ids, $fire_events = null); + + /** + * @param $data + * @return array|mixed|string + */ + final public function sanitarTag($data) + { + return APIhelpers::sanitarTag($this->modx->stripTags($data)); + } + + /** + * @param string $version + * @param bool $dmi3yy + * @return bool + */ + final protected function checkVersion($version, $dmi3yy = true) + { + $flag = false; + $currentVer = $this->modx->getVersionData('version'); + if (is_array($currentVer)) { + $currentVer = \APIHelpers::getkey($currentVer, 'version', ''); + } + $tmp = substr($currentVer, 0, strlen($version)); + if (version_compare($tmp, $version, '>=')) { + $flag = true; + if ($dmi3yy) { + $flag = $flag || (boolean) preg_match('/^' . $tmp . '(.*)\-d/', $currentVer); + } + } + + return $flag; + } + + /** + * @param string $name + * @return bool|string|int + */ + protected function eraseField($name) + { + $flag = false; + if (array_key_exists($name, $this->field)) { + $flag = $this->field[$name]; + unset($this->field[$name]); + } + + return $flag; + } + + /** + * Может ли содержать данное поле json массив + * @param string $field имя поля + * @return boolean + */ + public function isJsonField($field) + { + return (is_scalar($field) && in_array($field, $this->jsonFields)); + } + + /** + * Пометить поле как распакованное + * @param string $field имя поля + * @return $this + */ + public function markAsDecode($field) + { + if (is_scalar($field)) { + $this->_decodedFields->put($field, false); + } + + return $this; + } + + /** + * Пометить поле как запакованное + * @param string $field имя поля + * @return $this + */ + public function markAsEncode($field) + { + if (is_scalar($field)) { + $this->_decodedFields->put($field, true); + } + + return $this; + } + + /** + * Пометить все поля как запакованные + * @return $this + */ + public function markAllEncode() + { + $this->_decodedFields = new Collection(); + foreach ($this->jsonFields as $field) { + $this->markAsEncode($field); + } + + return $this; + } + + /** + * Пометить все поля как распакованные + * @return $this + */ + public function markAllDecode() + { + $this->_decodedFields = new Collection(); + foreach ($this->jsonFields as $field) { + $this->markAsDecode($field); + } + + return $this; + } + + /** + * Получить список не запакованных полей + * @return DLCollection + */ + public function getNoEncodeFields() + { + return $this->_decodedFields->filter(function ($value) { + return ($value === false); + }); + } + + /** + * Получить список не распакованных полей + * @return DLCollection + */ + public function getNoDecodeFields() + { + return $this->_decodedFields->filter(function ($value) { + return ($value === true); + }); + } + + /** + * Можно ли данное декодировать с помощью json_decode + * @param string $field имя поля + * @return boolean + */ + public function isDecodableField($field) + { + $data = $this->get($field); + + /** + * Если поле скалярного типа и оно не распаковывалось раньше + */ + + return (is_scalar($data) && is_scalar($field) && $this->_decodedFields->get($field) === true); + } + + /** + * Можно ли закодировать данные с помощью json_encode + * @param string $field имя поля + * @return boolean + */ + public function isEncodableField($field) + { + /** + * Если поле было распаковано ранее и еще не упаковано + */ + return (is_scalar($field) && $this->_decodedFields->get($field) === false); + } + + /** + * Декодирует конкретное поле + * @param string $field Имя поля + * @param bool $store обновить распакованное поле + * @return array ассоциативный массив с данными из json строки + */ + public function decodeField($field, $store = false) + { + $out = []; + if ($this->isDecodableField($field)) { + $data = $this->get($field); + $out = jsonHelper::jsonDecode($data, ['assoc' => true], true); + } + if ($store) { + $this->field[$field] = $out; + $this->markAsDecode($field); + } + + return $out; + } + + /** + * Декодирование всех json полей + * @return $this + */ + protected function decodeFields() + { + foreach ($this->getNoDecodeFields() as $field => $flag) { + $this->decodeField($field, true); + } + + return $this; + } + + /** + * Запаковывает конкретное поле в JSON + * @param string $field Имя поля + * @param bool $store обновить запакованное поле + * @return string|null json строка + */ + public function encodeField($field, $store = false) + { + $out = null; + if ($this->isEncodableField($field)) { + $data = $this->get($field); + $out = json_encode($data); + } + if ($store) { + $this->field[$field] = $out; + $this->markAsEncode($field); + } + + return $out; + } + + /** + * Запаковка всех json полей + * @return $this + */ + protected function encodeFields() + { + foreach ($this->getNoEncodeFields() as $field => $flag) { + $this->encodeField($field, true); + } + + return $this; + } + + /** + * @param mixed $data + * @param string $key + * @return bool + */ + protected function saveToCache($data, $key) + { + $out = false; + if ($this->cache) { + $out = $this->modx->cache->save($key, $data, 0); + } + + return $out; + } + + /** + * @param string $key + * @return mixed + */ + protected function loadFromCache($key) + { + $out = false; + if ($this->cache) { + $out = $this->modx->cache->fetch($key); + } + + return $out; + } + +} diff --git a/src/Traits/ContentTV.php b/src/Traits/ContentTV.php new file mode 100644 index 0000000..e613a81 --- /dev/null +++ b/src/Traits/ContentTV.php @@ -0,0 +1,146 @@ +modx->_TVnames = $this->loadFromCache('_TVnames'); + if ($this->modx->_TVnames === false || empty($this->modx->_TVnames) || $reload) { + $this->modx->_TVnames = []; + $result = $this->query('SELECT `id`,`name`,`default_text`,`type`,`display`,`display_params` FROM ' . $this->makeTable('site_tmplvars')); + while ($row = $this->modx->db->GetRow($result)) { + $this->modx->_TVnames[$row['name']] = [ + 'id' => $row['id'], + 'type' => $row['type'], + 'default' => $row['default_text'], + 'display' => $row['display'], + 'display_params' => $row['display_params'] + ]; + } + $this->saveToCache($this->modx->_TVnames, '_TVnames'); + } + $arrayTypes = ['checkbox', 'listbox-multiple']; + $arrayTVs = []; + foreach ($this->modx->_TVnames as $name => $data) { + $this->tvid[$data['id']] = $name; + $this->tv[$name] = $data['id']; + if (in_array($data['type'], $arrayTypes)) { + $arrayTVs[] = $name; + } + } + if (empty($this->tvaFields)) { + $this->tvaFields = $arrayTVs; + } + $this->loadTVTemplate()->loadTVDefault(array_values($this->tv)); + + return $this; + } + + /** + * @return $this + */ + protected function loadTVTemplate() + { + $this->tvTpl = $this->loadFromCache('_tvTpl'); + if ($this->tvTpl === false) { + $q = $this->query("SELECT `tmplvarid`, `templateid` FROM " . $this->makeTable('site_tmplvar_templates')); + $this->tvTpl = []; + while ($item = $this->modx->db->getRow($q)) { + $this->tvTpl[$item['templateid']][] = $item['tmplvarid']; + } + $this->saveToCache($this->tvTpl, '_tvTpl'); + } + + return $this; + } + + /** + * @param array $tvId + * @return $this + */ + protected function loadTVDefault(array $tvId = []) + { + if (is_array($tvId) && !empty($tvId)) { + $this->tvd = []; + foreach ($tvId as $id) { + $name = $this->tvid[$id]; + $this->tvd[$name] = $this->modx->_TVnames[$name]; + } + } + + return $this; + } + + /** + * @param $tvname + * @return null|string + */ + public function renderTV($tvname) + { + $out = null; + if ($this->getID() > 0) { + include_once MODX_MANAGER_PATH . "includes/tmplvars.format.inc.php"; + include_once MODX_MANAGER_PATH . "includes/tmplvars.commands.inc.php"; + $tvval = $this->get($tvname); + if ($this->isTVarrayField($tvname) && is_array($tvval)) { + $tvval = implode('||', $tvval); + } + $param = APIhelpers::getkey($this->tvd, $tvname, []); + $display = APIhelpers::getkey($param, 'display', ''); + $display_params = APIhelpers::getkey($param, 'display_params', ''); + $type = APIhelpers::getkey($param, 'type', ''); + $out = getTVDisplayFormat($tvname, $tvval, $display, $display_params, $type, $this->getID(), ''); + } + + return $out; + } + + /** + * @param $tvId + * @return bool + */ + protected function belongsToTemplate($tvId) + { + $template = $this->get('template'); + + return isset($this->tvTpl[$template]) && in_array($tvId, $this->tvTpl[$template]); + } + + /** + * Может ли содержать данное поле json массив + * @param string $field имя поля + * @return boolean + */ + public function isTVarrayField($field) + { + return (is_scalar($field) && in_array($field, $this->tvaFields)); + } +} diff --git a/src/Traits/RoleTV.php b/src/Traits/RoleTV.php new file mode 100644 index 0000000..46ce12b --- /dev/null +++ b/src/Traits/RoleTV.php @@ -0,0 +1,38 @@ +tvTpl = $this->loadFromCache('_tvRole'); + if ($this->tvTpl === false) { + $q = $this->query("SELECT `tmplvarid`, `roleid` FROM " . $this->makeTable('user_role_vars')); + $this->tvTpl = []; + while ($item = $this->modx->db->getRow($q)) { + $this->tvTpl[$item['roleid']][] = $item['tmplvarid']; + } + $this->saveToCache($this->tvTpl, '_tvRole'); + } + + return $this; + } + + /** + * @param $tvId + * @return bool + */ + protected function belongsToTemplate($tvId) + { + $template = $this->get('role'); + + return isset($this->tvTpl[$template]) && in_array($tvId, $this->tvTpl[$template]); + } +} \ No newline at end of file diff --git a/src/autoTable.php b/src/autoTable.php new file mode 100644 index 0000000..8c40d70 --- /dev/null +++ b/src/autoTable.php @@ -0,0 +1,138 @@ +table; + } + + /** + * autoTable constructor. + * @param DocumentParser $modx + * @param bool $debug + */ + public function __construct($modx, $debug = false) + { + parent::__construct($modx, $debug); + if (empty($this->default_field)) { + $data = $this->modx->db->getTableMetaData($this->makeTable($this->table)); + foreach ($data as $item) { + if (empty($this->pkName) && $item['Key'] == 'PRI') { + $this->pkName = $item['Field']; + } + if ($this->pkName != $item['Field']) { + $this->default_field[$item['Field']] = $item['Default']; + } + } + $this->generateField = true; + } + } + + /** + * @param $id + * @return autoTable + */ + public function edit($id) + { + $id = is_scalar($id) ? trim($id) : ''; + if ($this->getID() != $id) { + $this->close(); + $this->markAllEncode(); + $this->newDoc = false; + $result = $this->query("SELECT * from {$this->makeTable($this->table)} where `" . $this->pkName . "`='" . $this->escape($id) . "'"); + $this->fromArray($this->modx->db->getRow($result)); + $this->store($this->toArray()); + $this->id = $this->eraseField($this->pkName); + if (is_bool($this->id) && $this->id === false) { + $this->id = null; + } else { + $this->decodeFields(); + } + } + + return $this; + } + + /** + * @param bool $fire_events + * @param bool $clearCache + * @return bool|null|void + */ + public function save($fire_events = false, $clearCache = false) + { + foreach ($this->jsonFields as $field) { + if ($this->get($field) === null + && isset($this->default_field[$field]) + && is_array($this->default_field[$field])) { + $this->set($field, $this->default_field[$field]); + } + } + $fld = $this->encodeFields()->toArray(); + foreach ($this->default_field as $key => $value) { + if ($this->newDoc && $this->get($key) === null && $this->get($key) !== $value) { + $this->set($key, $value); + } + if ((!$this->generateField || isset($fld[$key])) && $this->get($key) !== null) { + $this->Uset($key); + } + unset($fld[$key]); + } + if (!empty($this->set)) { + if ($this->newDoc) { + $SQL = "INSERT {$this->ignoreError} INTO {$this->makeTable($this->table)} SET " . implode(', ', + $this->set); + } else { + $SQL = ($this->getID() === null) ? null : "UPDATE {$this->ignoreError} {$this->makeTable($this->table)} SET " . implode(', ', + $this->set) . " WHERE `" . $this->pkName . "` = " . $this->getID(); + } + $this->query($SQL); + if ($this->newDoc) { + $this->id = $this->modx->db->getInsertId(); + } + } + if ($clearCache) { + $this->clearCache($fire_events); + } + $this->decodeFields(); + + return $this->id; + } + + /** + * @param $ids + * @param bool $fire_events + * @return $this + * @throws Exception + */ + public function delete($ids, $fire_events = false) + { + $_ids = $this->cleanIDs($ids, ','); + if (is_array($_ids) && $_ids !== []) { + $id = $this->sanitarIn($_ids); + if (!empty($id)) { + $this->query("DELETE from {$this->makeTable($this->table)} where `" . $this->pkName . "` IN ({$id})"); + } + } else { + throw new Exception('Invalid IDs list for delete:
' . print_r($ids, 1) . '
'); + } + + return $this; + } +} diff --git a/src/modResource.php b/src/modResource.php new file mode 100644 index 0000000..e2357d9 --- /dev/null +++ b/src/modResource.php @@ -0,0 +1,944 @@ + 'document', + 'contentType' => 'text/html', + 'pagetitle' => 'New document', + 'longtitle' => '', + 'description' => '', + 'alias' => '', + 'link_attributes' => '', + 'published' => 1, + 'pub_date' => 0, + 'unpub_date' => 0, + 'parent' => 0, + 'isfolder' => 0, + 'introtext' => '', + 'content' => '', + 'richtext' => 1, + 'template' => 0, + 'menuindex' => 0, + 'searchable' => 1, + 'cacheable' => 1, + 'createdon' => 0, + 'createdby' => 0, + 'editedon' => 0, + 'editedby' => 0, + 'deleted' => 0, + 'deletedon' => 0, + 'deletedby' => 0, + 'publishedon' => 0, + 'publishedby' => 0, + 'menutitle' => '', + 'donthit' => 0, + 'privateweb' => 0, + 'privatemgr' => 0, + 'content_dispo' => 0, + 'hidemenu' => 0, + 'alias_visible' => 1 + ]; + /** + * @var array + */ + private $table = [ + '"' => '_', + "'" => '_', + ' ' => '_', + '.' => '_', + ',' => '_', + 'а' => 'a', + 'б' => 'b', + 'в' => 'v', + 'г' => 'g', + 'д' => 'd', + 'е' => 'e', + 'ё' => 'e', + 'ж' => 'zh', + 'з' => 'z', + 'и' => 'i', + 'й' => 'y', + 'к' => 'k', + 'л' => 'l', + 'м' => 'm', + 'н' => 'n', + 'о' => 'o', + 'п' => 'p', + 'р' => 'r', + 'с' => 's', + 'т' => 't', + 'у' => 'u', + 'ф' => 'f', + 'х' => 'h', + 'ц' => 'c', + 'ч' => 'ch', + 'ш' => 'sh', + 'щ' => 'sch', + 'ь' => '', + 'ы' => 'y', + 'ъ' => '', + 'э' => 'e', + 'ю' => 'yu', + 'я' => 'ya', + 'А' => 'A', + 'Б' => 'B', + 'В' => 'V', + 'Г' => 'G', + 'Д' => 'D', + 'Е' => 'E', + 'Ё' => 'E', + 'Ж' => 'Zh', + 'З' => 'Z', + 'И' => 'I', + 'Й' => 'Y', + 'К' => 'K', + 'Л' => 'L', + 'М' => 'M', + 'Н' => 'N', + 'О' => 'O', + 'П' => 'P', + 'Р' => 'R', + 'С' => 'S', + 'Т' => 'T', + 'У' => 'U', + 'Ф' => 'F', + 'Х' => 'H', + 'Ц' => 'C', + 'Ч' => 'Ch', + 'Ш' => 'Sh', + 'Щ' => 'Sch', + 'Ь' => '', + 'Ы' => 'Y', + 'Ъ' => '', + 'Э' => 'E', + 'Ю' => 'Yu', + 'Я' => 'Ya', + ]; + + /** @var array группы документов */ + protected $groupIds = []; + + /** + * modResource constructor. + * @param DocumentParser $modx + * @param bool $debug + */ + public function __construct($modx, $debug = false) + { + parent::__construct($modx, $debug); + $this->get_TV(); + } + + /** + * @return array + */ + public function toArrayMain() + { + $out = array_intersect_key(parent::toArray(), $this->default_field); + + return $out; + } + + /** + * @param bool $render + * @return array + */ + public function toArrayTV($render = false) + { + $out = array_diff_key(parent::toArray(), $this->default_field); + $tpl = $this->get('template'); + $tvTPL = APIhelpers::getkey($this->tvTpl, $tpl, []); + foreach ($tvTPL as $item) { + if (isset($this->tvid[$item]) && !array_key_exists($this->tvid[$item], $out)) { + $value = $this->get($this->tvid[$item]); + $out[$this->tvid[$item]] = empty($value) ? $this->tvd[$this->tvid[$item]] : $value; + } + + } + if ($render) { + foreach ($out as $key => $val) { + $out[$key] = $this->renderTV($key); + } + } + + return $out; + } + + /** + * @param string $prefix + * @param string $suffix + * @param string $sep + * @param bool $render + * @return array + */ + public function toArray($prefix = '', $suffix = '', $sep = '_', $render = false) + { + $out = array_merge( + $this->toArrayMain(), + $this->toArrayTV($render), + [$this->fieldPKName() => $this->getID()] + ); + + return APIhelpers::renameKeyArr($out, $prefix, $suffix, $sep); + } + + /** + * @return null|string + */ + public function getUrl() + { + $out = null; + $id = (int) $this->getID(); + if (!empty($id)) { + $out = UrlProcessor::makeUrl($id); + } + + return $out; + } + + /** + * @param string $main + * @param string $second + * @return mixed + */ + public function getTitle($main = 'menutitle', $second = 'pagetitle') + { + $title = $this->get($main); + if (empty($title) && $title !== '0') { + $title = $this->get($second); + } + + return $title; + } + + /** + * @return bool + */ + public function isWebShow() + { + $pub = ($this->get('publishedon') < time() && $this->get('published')); + $unpub = ($this->get('unpub_date') == 0 || $this->get('unpub_date') > time()); + $del = ($this->get('deleted') == 0 && ($this->get('deletedon') == 0 || $this->get('deletedon') > time())); + + return ($pub && $unpub && $del); + } + + /** + * @return $this + */ + public function touch() + { + $this->set('editedon', time()); + + return $this; + } + + /** + * @param $key + * @return mixed + */ + public function get($key) + { + $out = parent::get($key); + if (isset($this->tv[$key])) { + $tpl = $this->get('template'); + $tvTPL = APIhelpers::getkey($this->tvTpl, $tpl, []); + $tvID = APIhelpers::getkey($this->tv, $key, 0); + if (in_array($tvID, $tvTPL) && is_null($out)) { + $out = APIhelpers::getkey($this->tvd, $key, null); + $out = $out['default']; + } + } + + return $out; + } + + /** + * @param $key + * @param $value + * @return $this + */ + public function set($key, $value) + { + if ((is_scalar($value) || $this->isTVarrayField($key) || $this->isJsonField($key)) && is_scalar($key) && !empty($key)) { + switch ($key) { + case 'donthit': + $value = (int) ((bool) $value); + break; + case 'parent': + $value = (int) $value; + break; + case 'template': + $value = trim($value); + $value = $this->setTemplate($value); + break; + case 'published': + $value = (int) ((bool) $value); + if ($value) { + $this->field['publishedon'] = time() + $this->modxConfig('server_offset_time'); + } + break; + case 'pub_date': + $value = $this->getTime($value); + if ($value > 0 && time() + $this->modxConfig('server_offset_time') > $value) { + $this->field['published'] = 1; + $this->field['publishedon'] = $value; + } + break; + case 'unpub_date': + $value = $this->getTime($value); + if ($value > 0 && time() + $this->modxConfig('server_offset_time') > $value) { + $this->field['published'] = 0; + $this->field['publishedon'] = 0; + } + break; + case 'deleted': + $value = (int) ((bool) $value); + if ($value) { + $this->field['deletedon'] = time() + $this->modxConfig('server_offset_time'); + } else { + $this->field['deletedon'] = 0; + } + break; + case 'deletedon': + $value = $this->getTime($value); + if ($value > 0 && time() + $this->modxConfig('server_offset_time') < $value) { + $value = 0; + } + if ($value) { + $this->field['deleted'] = 1; + } + break; + case 'editedon': + case 'createdon': + case 'publishedon': + $value = $this->getTime($value); + break; + case 'publishedby': + case 'editedby': + case 'createdby': + case 'deletedby': + $value = (int) $value; + break; + } + $this->field[$key] = $value; + } + + return $this; + } + + /** + * @param array $data + * @return $this + */ + public function create($data = []) + { + $this->close(); + $fld = []; + foreach ($this->tvd as $name => $tv) { + $fld[$name] = $tv['default']; + }; + $this->store($fld); + + $this->fromArray(array_merge($fld, $data)); + $this->set('createdby', $this->getLoginUserID()) + ->set('createdon', $this->getTime(time())) + ->touch(); + + return $this; + } + + /** + * @param $id + * @return $this + */ + public function edit($id) + { + $id = is_scalar($id) ? trim($id) : ''; + if ($this->getID() != $id) { + $this->close(); + $this->markAllEncode(); + $this->newDoc = false; + $result = $this->query("SELECT * from {$this->makeTable('site_content')} where `id`=" . (int) $id); + $this->fromArray($this->modx->db->getRow($result)); + $result = $this->query("SELECT * from {$this->makeTable('site_tmplvar_contentvalues')} where `contentid`=" . (int) $id); + while ($row = $this->modx->db->getRow($result)) { + $this->field[$this->tvid[$row['tmplvarid']]] = $row['value']; + } + $fld = []; + foreach ($this->tvd as $name => $tv) { + if ($this->belongsToTemplate($this->tv[$name])) { + $fld[$name] = $tv['default']; + } + }; + $this->store(array_merge($fld, $this->field)); + if (empty($this->field['id'])) { + $this->id = null; + } else { + $this->id = $this->field['id']; + $this->set('editedby', null)->touch(); + $this->decodeFields(); + } + unset($this->field['id']); + } + + return $this; + } + + /** + * @param bool $fire_events + * @param bool $clearCache + * @return mixed + */ + public function save($fire_events = false, $clearCache = false) + { + $parent = null; + if ($this->field['pagetitle'] == '') { + $this->log['emptyPagetitle'] = 'Pagetitle is empty in
' . print_r($this->field, true) . '
'; + + return false; + } + + $uid = $this->modx->getLoginUserID('mgr'); + + if ( + empty($this->field['parent']) && + !$this->modxConfig('udperms_allowroot') && + !($uid && isset($_SESSION['mgrRole']) && $_SESSION['mgrRole'] == 1) + ) { + $this->log['rootForbidden'] = 'Only Administrators can create documents in the root folder because udperms_allowroot setting is off'; + + return false; + } + + $this->set('alias', $this->getAlias()); + + $this->invokeEvent('OnBeforeDocFormSave', [ + 'mode' => $this->newDoc ? "new" : "upd", + 'id' => isset($this->id) ? $this->id : '', + 'doc' => $this->toArray(), + 'docObj' => $this + ], $fire_events); + + $fld = $this->encodeFields()->toArray(null, null, null, false); + foreach ($this->default_field as $key => $value) { + $tmp = $this->get($key); + if ($this->newDoc && (!is_int($tmp) && $tmp == '')) { + if ($tmp == $value) { + switch ($key) { + case 'cacheable': + $value = (int) $this->modxConfig('cache_default'); + break; + case 'template': + $value = (int) $this->modxConfig('default_template'); + break; + case 'published': + $value = (int) $this->modxConfig('publish_default'); + break; + case 'searchable': + $value = (int) $this->modxConfig('search_default'); + break; + case 'donthit': + $value = (int) $this->modxConfig('track_visitors'); + break; + } + } + $this->field[$key] = $value; + } + switch (true) { + case $key == 'parent': + $parent = (int) $this->get($key); + $q = $this->query("SELECT count(`id`) FROM {$this->makeTable('site_content')} WHERE `id`='{$parent}'"); + if ($this->modx->db->getValue($q) != 1) { + $parent = 0; + } + $this->field[$key] = $parent; + $this->Uset($key); + break; + case ($key == 'alias_visible' && !$this->checkVersion('1.0.10', true)): + $this->eraseField('alias_visible'); + break; + default: + $this->Uset($key); + } + unset($fld[$key]); + } + + if (!empty($this->set)) { + if ($this->newDoc) { + $SQL = "INSERT into {$this->makeTable('site_content')} SET " . implode(', ', $this->set); + } else { + $SQL = "UPDATE {$this->makeTable('site_content')} SET " . implode(', ', + $this->set) . " WHERE `id` = " . $this->id; + } + $this->query($SQL); + + if ($this->newDoc) { + $this->id = $this->modx->db->getInsertId(); + } + + if ($parent > 0) { + $this->query("UPDATE {$this->makeTable('site_content')} SET `isfolder`='1' WHERE `id`='{$parent}'"); + } + } + + $_deleteTVs = $_insertTVs = []; + foreach ($fld as $key => $value) { + if (empty($this->tv[$key]) || !$this->isChanged($key) || !$this->belongsToTemplate($this->tv[$key])) { + continue; + } elseif ($value === '' || is_null($value) || (isset($this->tvd[$key]) && $value == $this->tvd[$key]['default'])) { + $_deleteTVs[] = $this->tv[$key]; + } else { + $_insertTVs[$this->tv[$key]] = $this->escape($value); + } + } + + if (!empty($_insertTVs)) { + $values = []; + foreach ($_insertTVs as $id => $value) { + $values[] = "({$this->id}, {$id}, '{$value}')"; + } + $values = implode(',', $values); + $this->query("INSERT INTO {$this->makeTable('site_tmplvar_contentvalues')} (`contentid`,`tmplvarid`,`value`) VALUES {$values} ON DUPLICATE KEY UPDATE + `value` = VALUES(`value`)"); + } + + if (!empty($_deleteTVs)) { + $ids = implode(',', $_deleteTVs); + $this->query("DELETE FROM {$this->makeTable('site_tmplvar_contentvalues')} WHERE `contentid` = '{$this->id}' AND `tmplvarid` IN ({$ids})"); + } + + if (!isset($this->mode)) { + $this->mode = $this->newDoc ? "new" : "upd"; + $this->newDoc = false; + } + + if (!empty($this->groupIds)) { + $this->setDocumentGroups($this->id, $this->groupIds); + } + $this->invokeEvent('OnDocFormSave', [ + 'mode' => $this->mode, + 'id' => isset($this->id) ? $this->id : '', + 'doc' => $this->toArray(), + 'docObj' => $this + ], $fire_events); + + + $this->modx->getAliasListing($this->id); + + if ($clearCache) { + $this->clearCache($fire_events); + } + $this->decodeFields(); + + return $this->id; + } + + /** + * @param $ids + * @return $this + * @throws Exception + */ + public function toTrash($ids) + { + $ignore = $this->systemID(); + $_ids = $this->cleanIDs($ids, ',', $ignore); + if (is_array($_ids) && $_ids != []) { + $id = $this->sanitarIn($_ids); + $uid = (int) $this->modx->getLoginUserId(); + $deletedon = time() + $this->modxConfig('server_offset_time'); + $this->query("UPDATE {$this->makeTable('site_content')} SET `deleted`=1, `deletedby`={$uid}, `deletedon`={$deletedon} WHERE `id` IN ({$id})"); + } else { + throw new Exception('Invalid IDs list for mark trash:
' . print_r($ids,
+                    1) . '
please, check ignore list:
' . print_r($ignore, 1) . '
'); + } + + return $this; + } + + /** + * @param bool $fire_events + * @return $this + */ + public function clearTrash($fire_events = false) + { + $q = $this->query("SELECT `id` FROM {$this->makeTable('site_content')} WHERE `deleted`='1'"); + $_ids = $this->modx->db->getColumn('id', $q); + if (is_array($_ids) && $_ids != []) { + $this->invokeEvent('OnBeforeEmptyTrash', [ + "ids" => $_ids + ], $fire_events); + + $id = $this->sanitarIn($_ids); + $this->query("DELETE from {$this->makeTable('site_content')} where `id` IN ({$id})"); + $this->query("DELETE from {$this->makeTable('site_tmplvar_contentvalues')} where `contentid` IN ({$id})"); + + $this->invokeEvent('OnEmptyTrash', [ + "ids" => $_ids + ], $fire_events); + } + + return $this; + } + + /** + * @param $ids + * @param int|bool $depth + * @return array + */ + public function children($ids, $depth) + { + $_ids = $this->cleanIDs($ids, ','); + if (is_array($_ids) && $_ids != []) { + $id = $this->sanitarIn($_ids); + if (!empty($id)) { + $q = $this->query("SELECT `id` FROM {$this->makeTable('site_content')} where `parent` IN ({$id})"); + $id = $this->modx->db->getColumn('id', $q); + if ($depth > 0 || $depth === true) { + $id = $this->children($id, is_bool($depth) ? $depth : ($depth - 1)); + } + $_ids = array_merge($_ids, $id); + } + } + + return $_ids; + } + + /** + * @param string|array $ids + * @param bool $fire_events + * @return $this + * @throws Exception + */ + public function delete($ids, $fire_events = false) + { + $ids = $this->children($ids, true); + $_ids = $this->cleanIDs($ids, ',', $this->systemID()); + $this->invokeEvent('OnBeforeDocFormDelete', [ + 'ids' => $_ids + ], $fire_events); + $this->toTrash($_ids); + $this->invokeEvent('OnDocFormDelete', [ + 'ids' => $_ids + ], $fire_events); + + return $this; + } + + /** + * @return array + */ + private function systemID() + { + $ignore = [ + 0, //empty document + (int) $this->modxConfig('site_start'), + (int) $this->modxConfig('error_page'), + (int) $this->modxConfig('unauthorized_page'), + (int) $this->modxConfig('site_unavailable_page') + ]; + $data = $this->query("SELECT DISTINCT setting_value FROM {$this->makeTable('web_user_settings')} WHERE `setting_name`='login_home' AND `setting_value`!=''"); + $data = $this->modx->db->makeArray($data); + foreach ($data as $item) { + $ignore[] = (int) $item['setting_value']; + } + + return array_unique($ignore); + + } + + /** + * @param $alias + * @return string + */ + protected function checkAlias($alias) + { + $alias = strtolower($alias); + if ($this->modxConfig('friendly_urls')) { + $_alias = $this->escape($alias); + if ((!$this->modxConfig('allow_duplicate_alias') && !$this->modxConfig('use_alias_path')) || ($this->modxConfig('allow_duplicate_alias') && $this->modxConfig('use_alias_path'))) { + $flag = $this->modx->db->getValue($this->query("SELECT `id` FROM {$this->makeTable('site_content')} WHERE `alias`='{$_alias}' AND `parent`={$this->get('parent')} LIMIT 1")); + } else { + $flag = $this->modx->db->getValue($this->query("SELECT `id` FROM {$this->makeTable('site_content')} WHERE `alias`='{$_alias}' LIMIT 1")); + } + if (($flag && $this->newDoc) || (!$this->newDoc && $flag && $this->id != $flag)) { + $suffix = substr($alias, -2); + if (preg_match('/-(\d+)/', $suffix, $tmp) && isset($tmp[1]) && (int) $tmp[1] > 1) { + $suffix = (int) $tmp[1] + 1; + $alias = substr($alias, 0, -2) . '-' . $suffix; + } else { + $alias .= '-2'; + } + $alias = $this->checkAlias($alias); + } + } + + return $alias; + } + + /** + * @param $key + * @return bool + */ + public function issetField($key) + { + return (array_key_exists($key, $this->default_field) || (array_key_exists($key, + $this->tv) && $this->belongsToTemplate($this->tv[$key]))); + } + + /** + * @param $tpl + * @return int + * @throws Exception + */ + public function setTemplate($tpl) + { + if (!is_numeric($tpl) || $tpl != (int) $tpl) { + if (is_scalar($tpl)) { + $sql = "SELECT `id` FROM {$this->makeTable('site_templates')} WHERE `templatename` = '" . $this->escape($tpl) . "'"; + $rs = $this->query($sql); + if (!$rs || $this->modx->db->getRecordCount($rs) <= 0) { + throw new Exception("Template {$tpl} is not exists"); + } + $tpl = $this->modx->db->getValue($rs); + } else { + throw new Exception("Invalid template name: " . print_r($tpl, 1)); + } + } + + return (int) $tpl; + } + + /** + * @return string + */ + protected function getAlias() + { + if ($this->modxConfig('friendly_urls') && $this->modxConfig('automatic_alias') && $this->get('alias') == '') { + $alias = strtr($this->get('pagetitle'), $this->table); + } else { + if ($this->get('alias') != '') { + $alias = $this->get('alias'); + } else { + $alias = ''; + } + } + $alias = $this->modx->stripAlias($alias); + + return $this->checkAlias($alias); + } + + /** + * @param int $parent + * @param string $criteria + * @param string $dir + * @return $this + * + * Пересчет menuindex по полю таблицы site_content + */ + public function updateMenuindex($parent, $criteria = 'id', $dir = 'asc') + { + $dir = strtolower($dir) == 'desc' ? 'desc' : 'asc'; + if (is_integer($parent) && $criteria !== '') { + $this->query("SET @index := 0"); + $this->query("UPDATE {$this->makeTable('site_content')} SET `menuindex` = (@index := @index + 1) WHERE `parent`={$parent} ORDER BY {$criteria} {$dir}"); + } + + return $this; + } + + /** + * Устанавливает значение шаблона согласно системной настройке + * + * @return $this + */ + public function setDefaultTemplate() + { + $parent = $this->get('parent'); + $template = $this->modxConfig('default_template'); + switch ($this->modxConfig('auto_template_logic')) { + case 'sibling': + if (!$parent) { + $site_start = $this->modxConfig('site_start'); + $where = "sc.isfolder=0 AND sc.id!={$site_start}"; + $sibl = $this->modx->getDocumentChildren($parent, 1, 0, 'template', $where, 'menuindex', 'ASC', 1); + if (isset($sibl[0]['template']) && $sibl[0]['template'] !== '') { + $template = $sibl[0]['template']; + } + } else { + $sibl = $this->modx->getDocumentChildren($parent, 1, 0, 'template', 'isfolder=0', 'menuindex', + 'ASC', 1); + if (isset($sibl[0]['template']) && $sibl[0]['template'] !== '') { + $template = $sibl[0]['template']; + } else { + $sibl = $this->modx->getDocumentChildren($parent, 0, 0, 'template', 'isfolder=0', 'menuindex', + 'ASC', 1); + if (isset($sibl[0]['template']) && $sibl[0]['template'] !== '') { + $template = $sibl[0]['template']; + } + } + } + break; + case 'parent': + if ($parent) { + $_parent = $this->modx->getPageInfo($parent, 0, 'template'); + if (isset($_parent['template'])) { + $template = $_parent['template']; + } + } + break; + } + $this->set('template', $template); + + return $this; + } + + /** + * Декодирует конкретное поле + * @param string $field Имя поля + * @param bool $store обновить распакованное поле + * @return array ассоциативный массив с данными из json строки + */ + public function decodeField($field, $store = false) + { + $out = []; + if ($this->isDecodableField($field)) { + $data = $this->get($field); + if ($this->isTVarrayField($field)) { + $out = explode('||', $data); + } else { + $out = jsonHelper::jsonDecode($data, ['assoc' => true], true); + } + } + if ($store) { + $this->field[$field] = $out; + $this->markAsDecode($field); + } + + return $out; + } + + /** + * Запаковывает конкретное поле в JSON + * @param string $field Имя поля + * @param bool $store обновить запакованное поле + * @return string|null json строка + */ + public function encodeField($field, $store = false) + { + $out = null; + if ($this->isEncodableField($field)) { + $data = $this->get($field); + if ($this->isTVarrayField($field)) { + $out = is_array($data) ? implode('||', $data) : (string) $data; + } else { + $out = json_encode($data); + } + } + if ($store) { + $this->field[$field] = $out; + $this->markAsEncode($field); + } + + return $out; + } + + /** + * Пометить все поля как запакованные + * @return $this + */ + public function markAllEncode() + { + parent::markAllEncode(); + foreach ($this->tvaFields as $field) { + $this->markAsEncode($field); + } + + return $this; + } + + /** + * Пометить все поля как распакованные + * @return $this + */ + public function markAllDecode() + { + parent::markAllDecode(); + foreach ($this->tvaFields as $field) { + $this->markAsDecode($field); + } + + return $this; + } + + /** + * @param int $docId + */ + public function getDocumentGroups($docId = 0) + { + $out = []; + $doc = $this->switchObject($docId); + if (null !== $doc->getID()) { + $doc_groups = $this->makeTable('document_groups'); + $docgroup_names = $this->makeTable('documentgroup_names'); + + $rs = $this->query("SELECT `dg`.`document_group`, `dgn`.`name` FROM {$doc_groups} as `dg` INNER JOIN {$docgroup_names} as `dgn` ON `dgn`.`id`=`dg`.`document_group` + WHERE `dg`.`document` = " . $doc->getID()); + while ($row = $this->modx->db->getRow($rs)) { + $out[$row['document_group']] = $row['name']; + } + + } + unset($doc); + + return $out; + } + + /** + * @param int $docId + * @param array $groupIds + * @return $this + */ + public function setDocumentGroups($docId = 0, $groupIds = []) + { + if (!is_array($groupIds)) { + return $this; + } + if ($this->newDoc && $docId == 0) { + $this->groupIds = $groupIds; + } else { + $doc = $this->switchObject($docId); + if ($id = $doc->getID()) { + foreach ($groupIds as $gid) { + $this->query("REPLACE INTO {$this->makeTable('document_groups')} (`document_group`, `document`) VALUES ('{$gid}', '{$id}')"); + } + if (!$this->newDoc) { + $groupIds = empty($groupIds) ? '0' : implode(',', $groupIds); + $this->query("DELETE FROM {$this->makeTable('document_groups')} WHERE `document`={$id} AND `document_group` NOT IN ({$groupIds})"); + } + } + unset($doc); + $this->groupIds = []; + } + + return $this; + } +} diff --git a/src/modUsers.php b/src/modUsers.php new file mode 100644 index 0000000..f44f60c --- /dev/null +++ b/src/modUsers.php @@ -0,0 +1,872 @@ + [ + 'username' => '', + 'password' => '', + 'cachepwd' => '', + 'refresh_token' => '', + 'access_token' => '', + 'valid_to' => null, + 'verified_key' => '' + ], + 'attribute' => [ + 'fullname' => '', + 'first_name' => '', + 'last_name' => '', + 'middle_name' => '', + 'role' => 0, + 'email' => '', + 'phone' => '', + 'mobilephone' => '', + 'blocked' => 0, + 'blockeduntil' => 0, + 'blockedafter' => 0, + 'logincount' => 0, + 'lastlogin' => 0, + 'thislogin' => 0, + 'failedlogincount' => 0, + 'sessionid' => '', + 'dob' => 0, + 'gender' => 0, + 'country' => '', + 'state' => '', + 'city' => '', + 'street' => '', + 'zip' => '', + 'fax' => '', + 'photo' => '', + 'comment' => '', + 'createdon' => 0, + 'editedon' => 0, + 'verified' => 0 + ], + 'hidden' => [ + 'internalKey' + ] + ]; + + /** + * @var string + */ + protected $givenPassword = ''; + protected $groupIds = []; + protected $userIdCache = [ + 'attribute.internalKey' => '', + 'attribute.email' => '', + 'user.username' => '' + ]; + + /** + * @var integer + */ + private $rememberTime; + + protected $context = 'web'; + + /** + * MODxAPI constructor. + * @param DocumentParser $modx + * @param bool $debug + * @throws Exception + */ + public function __construct(DocumentParser $modx, $debug = false) + { + $this->setRememberTime(60 * 60 * 24 * 365 * 5); + parent::__construct($modx, $debug); + $this->get_TV(); + } + + /** + * @param $val + * @return $this + */ + protected function setRememberTime($val) + { + $this->rememberTime = (int) $val; + return $this; + } + + /** + * @return integer + */ + public function getRememberTime() + { + return $this->rememberTime; + } + + /** + * @param $key + * @return bool + */ + public function issetField($key) + { + return ( + array_key_exists($key, $this->default_field['user']) + || array_key_exists($key, $this->default_field['attribute']) + || in_array($key, $this->default_field['hidden']) + || (array_key_exists($key, $this->tv) && $this->belongsToTemplate($this->tv[$key])) + ); + } + + /** + * @param string $data + * @return string|false + */ + protected function findUser($data) + { + switch (true) { + case (is_int($data)): + $find = 'attribute.internalKey'; + break; + case filter_var($data, FILTER_VALIDATE_EMAIL): + $find = 'attribute.email'; + break; + case is_scalar($data): + $find = 'user.username'; + break; + default: + $find = false; + } + + return $find; + } + + /** + * @param array $data + * @return $this + */ + public function create($data = []) + { + parent::create($data); + $this->set('createdon', time()); + $fld = []; + foreach ($this->tvd as $name => $tv) { + $fld[$name] = $tv['default']; + }; + $this->store($fld); + $this->fromArray(array_merge($fld, $data)); + + return $this; + } + + /** + * + */ + public function close() + { + parent::close(); + $this->userIdCache = [ + 'attribute.internalKey' => '', + 'attribute.email' => '', + 'user.username' => '' + ]; + } + + /** + * @param $id + * @return mixed + */ + protected function getUserId($id) + { + $find = $this->findUser($id); + if ($find && !empty($this->userIdCache[$find])) { + $id = $this->userIdCache[$find]; + } else { + $id = null; + } + + return $id; + } + + /** + * @param $id + * @return $this + */ + public function edit($id) + { + if (!is_int($id)) { + $id = is_scalar($id) ? trim($id) : ''; + } + if ($this->getUserId($id) != $id) { + $this->close(); + $this->markAllEncode(); + $this->newDoc = false; + + if (!$find = $this->findUser($id)) { + $this->id = null; + } else { + $this->editQuery($find, $id); + if (empty($this->field['internalKey'])) { + $this->id = null; + } else { + $this->id = $this->field['id']; + $this->set('editedon', time()); + } + $this->loadUserSettings(); + $this->loadUserTVs(); + $this->decodeFields(); + $this->store($this->field); + $this->userIdCache['attribute.internalKey'] = $this->getID(); + $this->userIdCache['attribute.email'] = $this->get('email'); + $this->userIdCache['user.username'] = $this->get('username'); + unset($this->field['id']); + unset($this->field['internalKey']); + } + } + + return $this; + } + + protected function loadUserTVs() + { + $id = (int) $this->get('internalKey'); + $result = $this->query("SELECT * from {$this->makeTable('user_values')} where `userid`=" . (int) $id); + while ($row = $this->modx->db->getRow($result)) { + if ($this->belongsToTemplate($row['tmplvarid'])) { + $tv = $this->tvid[$row['tmplvarid']]; + $this->field[$tv] = empty($row['value']) ? $this->tvd[$tv]['default'] : $row['value']; + } + } + } + + protected function loadUserSettings() + { + $webUser = $this->getID(); + + if (!empty($webUser)) { + $settings = $this->modx->db->makeArray($this->modx->db->select('*', $this->makeTable('user_settings'), + "user = {$webUser}")); + $this->fromArray(array_column($settings, 'setting_value', 'setting_name')); + } + } + + /** + * @param string $find + * @param string $id + */ + protected function editQuery($find, $id) + { + $result = $this->query(" + SELECT * from {$this->makeTable('user_attributes')} as attribute + LEFT JOIN {$this->makeTable('users')} as user ON user.id=attribute.internalKey + WHERE {$find}='{$this->escape($id)}' + "); + $this->field = $this->modx->db->getRow($result); + } + + /** + * @param string $key + * @param $value + * @return $this + */ + public function set($key, $value) + { + if ((is_scalar($value) || $this->isTVarrayField($key) || $this->isJsonField($key)) && is_scalar($key) && !empty($key)) { + switch ($key) { + case 'password': + $this->givenPassword = $value; + $value = $this->getPassword($value); + break; + case 'sessionid': + //short bug fix when authoring a web user if the manager is logged in + $oldSessionId = session_id(); + session_regenerate_id(false); + $value = session_id(); + if ($mid = $this->modx->getLoginUserID('mgr')) { + //short bug fix when authoring a web user if the manager is logged in + $this->modx->db->delete($this->makeTable('active_users'), + "`internalKey`={$mid} and `sid` != '{$oldSessionId}' "); + $this->modx->db->delete($this->makeTable('active_user_sessions'), + "`internalKey`={$mid} and `sid` != '{$oldSessionId}' "); + + $this->modx->db->query("UPDATE {$this->makeTable('active_user_locks')} SET `sid`='{$value}' WHERE `internalKey`={$mid}"); + $this->modx->db->query("UPDATE {$this->makeTable('active_user_sessions')} SET `sid`='{$value}' WHERE `internalKey`={$mid}"); + $this->modx->db->query("UPDATE {$this->makeTable('active_users')} SET `sid`='{$value}' WHERE `internalKey`={$mid}"); + } + break; + case 'editedon': + case 'createdon': + $value = $this->getTime($value); + break; + } + $this->field[$key] = $value; + } + + return $this; + } + + /** + * @param string $prefix + * @param string $suffix + * @param string $sep + * @param false $render + * @return array + */ + public function toArray($prefix = '', $suffix = '', $sep = '_', $render = false) + { + $out = parent::toArray($prefix, $suffix, $sep, $render); + if ($render) { + $tpl = $this->get('role'); + $tvTPL = APIhelpers::getkey($this->tvTpl, $tpl, []); + foreach ($tvTPL as $item) { + if (isset($this->tvid[$item]) && array_key_exists($this->tvid[$item], $out)) { + $out[$this->tvid[$item]] = $this->renderTV($this->tvid[$item]); + } + } + } + + return $out; + } + + + /** + * @param $key + * @return mixed + */ + public function get($key) + { + $out = parent::get($key); + if (isset($this->tv[$key])) { + $tpl = $this->get('role'); + $tvTPL = APIhelpers::getkey($this->tvTpl, $tpl, []); + $tvID = APIhelpers::getkey($this->tv, $key, 0); + if (!in_array($tvID, $tvTPL)) { + $out = null; + } + } + + return $out; + } + + /** + * @param $pass + * @return string + */ + public function getPassword($pass) + { + return $this->modx->getPasswordHash()->HashPassword($pass); + } + + /** + * @param bool $fire_events + * @param bool $clearCache + * @return bool|int|null|void + */ + public function save($fire_events = false, $clearCache = false) + { + if ($this->get('email') == '' || $this->get('username') == '' || $this->get('password') == '') { + $this->log['EmptyPKField'] = 'Email, username or password is empty
' . print_r(
+                    $this->toArray(),
+                    true
+                ) . '
'; + + return false; + } + + if ($this->isChanged('username') && !$this->checkUnique('users', 'username')) { + $this->log['UniqueUsername'] = 'username not unique
' . print_r(
+                    $this->get('username'),
+                    true
+                ) . '
'; + + return false; + } + + if ($this->isChanged('username') && !$this->checkUnique('user_attributes', 'email', 'internalKey')) { + $this->log['UniqueEmail'] = 'Email not unique
' . print_r($this->get('email'), true) . '
'; + + return false; + } + $this->set('sessionid', ''); + $fld = $this->encodeFields()->toArray(); + foreach ($this->default_field['user'] as $key => $value) { + $tmp = $this->get($key); + if ($this->newDoc && (!is_int($tmp) && $tmp == '')) { + $this->field[$key] = $value; + } + $this->Uset($key, 'user'); + unset($fld[$key]); + } + if (!empty($this->set['user'])) { + if ($this->newDoc) { + $SQL = "INSERT into {$this->makeTable('users')} SET " . implode(', ', $this->set['user']); + } else { + $SQL = "UPDATE {$this->makeTable('users')} SET " . implode( + ', ', + $this->set['user'] + ) . " WHERE id = " . $this->id; + } + $this->query($SQL); + } + + if ($this->newDoc) { + $this->id = (int) $this->modx->db->getInsertId(); + } + + $this->saveQuery($fld); + unset($fld['id']); + + if (!$this->newDoc && $this->givenPassword) { + $this->invokeEvent(self::ON_CHANGE_PASSWORD_EVENT, [ + 'userObj' => $this, + 'userid' => $this->id, + 'user' => $this->toArray(), + 'userpassword' => $this->givenPassword, + 'internalKey' => $this->id, + 'username' => $this->get('username') + ], $fire_events); + } + + if (!empty($this->groupIds)) { + $this->setUserGroups($this->id, $this->groupIds); + } + + $this->invokeEvent(self::ON_SAVE_USER_EVENT, [ + 'userObj' => $this, + 'mode' => $this->newDoc ? "new" : "upd", + 'id' => $this->id, + 'user' => $this->toArray() + ], $fire_events); + + if ($clearCache) { + $this->clearCache($fire_events); + } + $this->decodeFields(); + + return $this->id; + } + + /** + * @param array $fld + */ + protected function saveQuery(array &$fld) + { + foreach ($this->default_field['attribute'] as $key => $value) { + $tmp = $this->get($key); + if ($this->newDoc && (!is_int($tmp) && $tmp == '')) { + $this->field[$key] = $value; + } + $this->Uset($key, 'attribute'); + unset($fld[$key]); + } + if (!empty($this->set['attribute'])) { + if ($this->newDoc) { + $this->set('internalKey', $this->id)->Uset('internalKey', 'attribute'); + $SQL = "INSERT into {$this->makeTable('user_attributes')} SET " . implode( + ', ', + $this->set['attribute'] + ); + } else { + $SQL = "UPDATE {$this->makeTable('user_attributes')} SET " . implode( + ', ', + $this->set['attribute'] + ) . " WHERE internalKey = " . $this->getID(); + } + $this->query($SQL); + } + $_deleteTVs = $_insertTVs = []; + foreach ($fld as $key => $value) { + if (empty($this->tv[$key]) || !$this->isChanged($key) || !$this->belongsToTemplate($this->tv[$key])) { + continue; + } elseif ($value === '' || is_null($value) || (isset($this->tvd[$key]) && $value == $this->tvd[$key]['default'])) { + $_deleteTVs[] = $this->tv[$key]; + } else { + $_insertTVs[$this->tv[$key]] = $this->escape($value); + } + } + + if (!empty($_insertTVs)) { + $values = []; + foreach ($_insertTVs as $id => $value) { + $values[] = "({$this->id}, {$id}, '{$value}')"; + } + $values = implode(',', $values); + $this->query("INSERT INTO {$this->makeTable('user_values')} (`userid`,`tmplvarid`,`value`) VALUES {$values} ON DUPLICATE KEY UPDATE + `value` = VALUES(`value`)"); + } + + if (!empty($_deleteTVs)) { + $ids = implode(',', $_deleteTVs); + $this->query("DELETE FROM {$this->makeTable('user_values')} WHERE `userid` = '{$this->id}' AND `tmplvarid` IN ({$ids})"); + } + + } + + /** + * @param $ids + * @param bool $fire_events + * @return bool|null|void + */ + public function delete($ids, $fire_events = false) + { + if ($this->edit($ids)) { + $flag = $this->deleteQuery(); + $this->query("DELETE FROM {$this->makeTable('user_values')} WHERE `userid`='{$this->getID()}'"); + $this->query("DELETE FROM {$this->makeTable('member_groups')} WHERE `member`='{$this->getID()}'"); + $this->invokeEvent(self::ON_DELETE_USER_EVENT, [ + 'userObj' => $this, + 'userid' => $this->getID(), + 'internalKey' => $this->getID(), + 'username' => $this->get('username'), + 'timestamp' => time() + ], $fire_events); + } else { + $flag = false; + } + $this->close(); + + return $flag; + } + + /** + * @return mixed + */ + protected function deleteQuery() + { + return $this->query(" + DELETE user,attribute FROM {$this->makeTable('user_attributes')} as attribute + LEFT JOIN {$this->makeTable('users')} as user ON user.id=attribute.internalKey + WHERE attribute.internalKey='{$this->escape($this->getID())}'"); + } + + /** + * @param int $id + * @param bool|integer $fulltime + * @param string $cookieName + * @param bool $fire_events + * @return bool + */ + public function authUser($id = 0, $fulltime = true, $cookieName = 'WebLoginPE', $fire_events = false) + { + $flag = false; + if (null === $this->getID() && $id) { + $this->edit($id); + } + if (null !== $this->getID()) { + $flag = true; + $this->set('refresh_token', hash('sha256', Str::random(32))); + $this->set('access_token', hash('sha256', Str::random(32))); + $this->set('valid_to', Carbon::now()->addHours(11)); + $this->save(false); + $this->SessionHandler('start', $cookieName, $fulltime); + $this->invokeEvent(self::ON_USER_LOGIN_EVENT, [ + 'userObj' => $this, + 'userid' => $this->getID(), + 'username' => $this->get('username'), + 'userpassword' => $this->givenPassword, + 'rememberme' => $fulltime + ], $fire_events); + } + + return $flag; + } + + /** + * @param int $id + * @return bool + */ + public function checkBlock($id = 0) + { + if ($this->getID()) { + $tmp = clone $this; + } else { + $tmp = $this; + } + if ($id && $tmp->getUserId($id) != $id) { + $tmp->edit($id); + } + $now = time(); + + $b = $tmp->get('blocked'); + $bu = $tmp->get('blockeduntil'); + $ba = $tmp->get('blockedafter'); + $flag = (($b && !$bu && !$ba) || ($bu && $now < $bu) || ($ba && $now > $ba)); + unset($tmp); + + return $flag; + } + + /** + * @param $id + * @param $password + * @param $blocker + * @param bool $fire_events + * @return bool + */ + public function testAuth($id, $password, $blocker, $fire_events = false) + { + if ($this->getID()) { + $tmp = clone $this; + } else { + $tmp = $this; + } + if ($id && $tmp->getUserId($id) != $id) { + $tmp->edit($id); + } + + $flag = $pluginFlag = false; + if ((null !== $tmp->getID()) && (!$blocker || ($blocker && !$tmp->checkBlock($id))) + ) { + $eventResult = $this->getInvokeEventResult(self::ON_USER_AUTHENTICATION_EVENT, [ + 'userObj' => $this, + 'userid' => $tmp->getID(), + 'username' => $tmp->get('username'), + 'userpassword' => $password, + 'savedpassword' => $tmp->get('password') + ], $fire_events); + if (is_array($eventResult)) { + foreach ($eventResult as $result) { + $pluginFlag = (bool) $result; + } + } else { + $pluginFlag = (bool) $eventResult; + } + if (!$pluginFlag) { + $flag = ($tmp->get('password') == $tmp->getPassword($password)); + } + } + unset($tmp); + + return $flag || $pluginFlag; + } + + /** + * @param bool|integer $fulltime + * @param string $cookieName + * @return bool + */ + public function AutoLogin($fulltime = true, $cookieName = 'WebLoginPE', $fire_events = null) + { + $flag = false; + if (isset($_COOKIE[$cookieName])) { + $cookie = explode('|', $_COOKIE[$cookieName], 4); + if (isset($cookie[0], $cookie[1], $cookie[2]) && strlen($cookie[0]) == 32 && strlen($cookie[1]) == 32) { + if (!$fulltime && isset($cookie[4])) { + $fulltime = (int) $cookie[4]; + } + $this->close(); + $q = $this->modx->db->query("SELECT id FROM " . $this->makeTable('web_users') . " WHERE md5(username)='{$this->escape($cookie[0])}'"); + $id = $this->modx->db->getValue($q); + if ($this->edit($id) + && null !== $this->getID() + && $this->get('password') == $cookie[1] + && $this->get('sessionid') == $cookie[2] + && !$this->checkBlock($this->getID()) + ) { + $flag = $this->authUser($this->getID(), $fulltime, $cookieName, $fire_events); + } + } + } + + return $flag; + } + + /** + * @param string $cookieName + * @param bool $fire_events + */ + public function logOut($cookieName = 'WebLoginPE', $fire_events = false) + { + if (!$uid = $this->modx->getLoginUserID('web')) { + return; + } + if ($this->edit($uid)->getID()) { + $this->fromArray([ + 'refresh_token' => '', + 'access_token' => '', + 'valid_until' => null + ])->save(false, false); + } + $params = [ + 'username' => $_SESSION['webShortname'], + 'internalKey' => $uid, + 'userid' => $uid // Bugfix by TS + ]; + $this->invokeEvent('OnBeforeWebLogout', $params, $fire_events); + $this->SessionHandler('destroy', $cookieName ? $cookieName : 'WebLoginPE'); + $this->invokeEvent('OnWebLogout', $params, $fire_events); + } + + /** + * SessionHandler + * Starts the user session on login success. Destroys session on error or logout. + * + * @param string $directive ('start' or 'destroy') + * @param string $cookieName + * @param bool|integer $remember + * @return modUsers + * @author Raymond Irving + * @author Scotty Delicious + * + * remeber может быть числом в секундах + */ + protected function SessionHandler($directive, $cookieName, $remember = true) + { + switch ($directive) { + case 'start': + if ($this->getID() !== null) { + $_SESSION['webShortname'] = $this->get('username'); + $_SESSION['webFullname'] = $this->get('fullname'); + $_SESSION['webEmail'] = $this->get('email'); + $_SESSION['webValidated'] = 1; + $_SESSION['webInternalKey'] = $this->getID(); + $_SESSION['webFailedlogins'] = $this->get('failedlogincount'); + $_SESSION['webLastlogin'] = $this->get('lastlogin'); + $_SESSION['webLogincount'] = $this->get('logincount'); + $_SESSION['webUsrConfigSet'] = []; + $_SESSION['webUserGroupNames'] = $this->getUserGroups(); + $_SESSION['webDocGroups'] = $this->getDocumentGroups(); + if (!empty($remember)) { + $this->setAutoLoginCookie($cookieName, $remember); + } + } + break; + case 'destroy': + if (isset($_SESSION['webValidated'])) { + unset($_SESSION['webShortname']); + unset($_SESSION['webFullname']); + unset($_SESSION['webEmail']); + unset($_SESSION['webValidated']); + unset($_SESSION['webInternalKey']); + unset($_SESSION['webFailedlogins']); + unset($_SESSION['webLastlogin']); + unset($_SESSION['webLogincount']); + unset($_SESSION['webUsrConfigSet']); + unset($_SESSION['webUserGroupNames']); + unset($_SESSION['webDocGroups']); + + setcookie($cookieName, '', time() - 60, MODX_BASE_URL); + } + break; + } + + return $this; + } + + /** + * @return bool + */ + public function isSecure() + { + return strpos(MODX_SITE_URL, 'https') === 0; + } + + /** + * @param $cookieName + * @param bool|integer $remember + * @return $this + */ + public function setAutoLoginCookie($cookieName, $remember = true) + { + if (!empty($cookieName) && $this->getID() !== null) { + $secure = $this->isSecure(); + $remember = is_bool($remember) ? $this->getRememberTime() : (int) $remember; + $cookieValue = [md5($this->get('username')), $this->get('password'), $this->get('sessionid'), $remember]; + $cookieValue = implode('|', $cookieValue); + $cookieExpires = time() + $remember; + setcookie($cookieName, $cookieValue, $cookieExpires, MODX_BASE_URL, '', $secure, true); + } + + return $this; + } + + /** + * @param int $userID + * @return array + */ + public function getDocumentGroups($userID = 0) + { + $out = []; + $user = $this->switchObject($userID); + if (null !== $user->getID()) { + $groups = $this->modx->getFullTableName('member_groups'); + $group_access = $this->modx->getFullTableName('membergroup_access'); + + $sql = "SELECT `uga`.`documentgroup` FROM {$groups} as `ug` + INNER JOIN {$group_access} as `uga` ON `uga`.`membergroup`=`ug`.`user_group` + WHERE `ug`.`member` = " . $user->getID(); + $out = $this->modx->db->getColumn('documentgroup', $this->query($sql)); + } + unset($user); + + return $out; + } + + /** + * @param int $userID + * @return array + */ + public function getUserGroups($userID = 0) + { + $out = []; + $user = $this->switchObject($userID); + if (null !== $user->getID()) { + $groups = $this->makeTable('member_groups'); + $group_names = $this->makeTable('membergroup_names'); + + $rs = $this->query("SELECT `ug`.`user_group`, `ugn`.`name` FROM {$groups} as `ug` + INNER JOIN {$group_names} as `ugn` ON `ugn`.`id`=`ug`.`user_group` + WHERE `ug`.`member` = " . $user->getID()); + while ($row = $this->modx->db->getRow($rs)) { + $out[$row['user_group']] = $row['name']; + } + } + unset($user); + + return $out; + } + + /** + * @param int $userID + * @param array $groupIds + * @return $this + */ + public function setUserGroups($userID = 0, $groupIds = []) + { + if (!is_array($groupIds)) { + return $this; + } + if ($this->newDoc && $userID == 0) { + $this->groupIds = $groupIds; + } else { + $user = $this->switchObject($userID); + if ($uid = $user->getID()) { + foreach ($groupIds as $gid) { + $this->query("REPLACE INTO {$this->makeTable('member_groups')} (`user_group`, `member`) VALUES ('{$gid}', '{$uid}')"); + } + if (!$this->newDoc) { + $groupIds = empty($groupIds) ? '0' : implode(',', $groupIds); + $this->query("DELETE FROM {$this->makeTable('member_groups')} WHERE `member`={$uid} AND `user_group` NOT IN ({$groupIds})"); + } + } + unset($user); + $this->groupIds = []; + } + + return $this; + } +}