Windows 10

Hibernate. Основные принципы работы с сессиями и транзакциями

Hibernate. Основные принципы работы с сессиями и транзакциями

Получение частных данных не всегда означает взлом - иногда они опубликованы в общем доступе. Знание настроек Google и немного смекалки позволят найти массу интересного - от номеров кредиток до документов ФБР.

WARNING

Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.

К интернету сегодня подключают всё подряд, мало заботясь об ограничении доступа. Поэтому многие приватные данные становятся добычей поисковиков. Роботы-«пауки» уже не ограничиваются веб-страницами, а индексируют весь доступный в Сети контент и постоянно добавляют в свои базы не предназначенную для разглашения информацию. Узнать эти секреты просто - нужно лишь знать, как именно спросить о них.

Ищем файлы

В умелых руках Google быстро найдет все, что плохо лежит в Сети, - например, личную информацию и файлы для служебного использования. Их частенько прячут, как ключ под половиком: настоящих ограничений доступа нет, данные просто лежат на задворках сайта, куда не ведут ссылки. Стандартный веб-интерфейс Google предоставляет лишь базовые настройки расширенного поиска, но даже их будет достаточно.

Ограничить поиск по файлам определенного вида в Google можно с помощью двух операторов: filetype и ext . Первый задает формат, который поисковик определил по заголовку файла, второй - расширение файла, независимо от его внутреннего содержимого. При поиске в обоих случаях нужно указывать лишь расширение. Изначально оператор ext было удобно использовать в тех случаях, когда специфические признаки формата у файла отсутствовали (например, для поиска конфигурационных файлов ini и cfg, внутри которых может быть все что угодно). Сейчас алгоритмы Google изменились, и видимой разницы между операторами нет - результаты в большинстве случаев выходят одинаковые.


Фильтруем выдачу

По умолчанию слова и вообще любые введенные символы Google ищет по всем файлам на проиндексированных страницах. Ограничить область поиска можно по домену верхнего уровня, конкретному сайту или по месту расположения искомой последовательности в самих файлах. Для первых двух вариантов используется оператор site, после которого вводится имя домена или выбранного сайта. В третьем случае целый набор операторов позволяет искать информацию в служебных полях и метаданных. Например, allinurl отыщет заданное в теле самих ссылок, allinanchor - в тексте, снабженном тегом , allintitle - в заголовках страниц, allintext - в теле страниц.

Для каждого оператора есть облегченная версия с более коротким названием (без приставки all). Разница в том, что allinurl отыщет ссылки со всеми словами, а inurl - только с первым из них. Второе и последующие слова из запроса могут встречаться на веб-страницах где угодно. Оператор inurl тоже имеет отличия от другого схожего по смыслу - site . Первый также позволяет находить любую последовательность символов в ссылке на искомый документ (например, /cgi-bin/), что широко используется для поиска компонентов с известными уязвимостями.

Попробуем на практике. Берем фильтр allintext и делаем так, чтобы запрос выдал список номеров и проверочных кодов кредиток, срок действия которых истечет только через два года (или когда их владельцам надоест кормить всех подряд).

Allintext: card number expiration date /2017 cvv

Когда читаешь в новостях, что юный хакер «взломал серверы» Пентагона или NASA, украв секретные сведения, то в большинстве случаев речь идет именно о такой элементарной технике использования Google. Предположим, нас интересует список сотрудников NASA и их контактные данные. Наверняка такой перечень есть в электронном виде. Для удобства или по недосмотру он может лежать и на самом сайте организации. Логично, что в этом случае на него не будет ссылок, поскольку предназначен он для внутреннего использования. Какие слова могут быть в таком файле? Как минимум - поле «адрес». Проверить все эти предположения проще простого.


Inurl:nasa.gov filetype:xlsx "address"


Пользуемся бюрократией

Подобные находки - приятная мелочь. По-настоящему же солидный улов обеспечивает более детальное знание операторов Google для веб-мастеров, самой Сети и особенностей структуры искомого. Зная детали, можно легко отфильтровать выдачу и уточнить свойства нужных файлов, чтобы в остатке получить действительно ценные данные. Забавно, что здесь на помощь приходит бюрократия. Она плодит типовые формулировки, по которым удобно искать случайно просочившиеся в Сеть секретные сведения.

Например, обязательный в канцелярии министерства обороны США штамп Distribution statement означает стандартизированные ограничения на распространение документа. Литерой A отмечаются публичные релизы, в которых нет ничего секретного; B - предназначенные только для внутреннего использования, C - строго конфиденциальные и так далее до F. Отдельно стоит литера X, которой отмечены особо ценные сведения, представляющие государственную тайну высшего уровня. Пускай такие документы ищут те, кому это положено делать по долгу службы, а мы ограничимся файлами с литерой С. Согласно директиве DoDI 5230.24, такая маркировка присваивается документам, содержащим описание критически важных технологий, попадающих под экспортный контроль. Обнаружить столь тщательно охраняемые сведения можно на сайтах в домене верхнего уровня.mil, выделенного для армии США.

"DISTRIBUTION STATEMENT C" inurl:navy.mil

Очень удобно, что в домене.mil собраны только сайты из ведомства МО США и его контрактных организаций. Поисковая выдача с ограничением по домену получается исключительно чистой, а заголовки - говорящими сами за себя. Искать подобным образом российские секреты практически бесполезно: в доменах.ru и.рф царит хаос, да и названия многих систем вооружения звучат как ботанические (ПП «Кипарис», САУ «Акация») или вовсе сказочные (ТОС «Буратино»).


Внимательно изучив любой документ с сайта в домене.mil, можно увидеть и другие маркеры для уточнения поиска. Например, отсылку к экспортным ограничениям «Sec 2751», по которой также удобно искать интересную техническую информацию. Время от времени ее изымают с официальных сайтов, где она однажды засветилась, поэтому, если в поисковой выдаче не удается перейти по интересной ссылке, воспользуйся кешем Гугла (оператор cache) или сайтом Internet Archive.

Забираемся в облака

Помимо случайно рассекреченных документов правительственных ведомств, в кеше Гугла временами всплывают ссылки на личные файлы из Dropbox и других сервисов хранения данных, которые создают «приватные» ссылки на публично опубликованные данные. С альтернативными и самодельными сервисами еще хуже. Например, следующий запрос находит данные всех клиентов Verizon, у которых на роутере установлен и активно используется FTP-сервер.

Allinurl:ftp:// verizon.net

Таких умников сейчас нашлось больше сорока тысяч, а весной 2015-го их было на порядок больше. Вместо Verizon.net можно подставить имя любого известного провайдера, и чем он будет известнее, тем крупнее может быть улов. Через встроенный FTP-сервер видно файлы на подключенном к маршрутизатору внешнем накопителе. Обычно это NAS для удаленной работы, персональное облако или какая-нибудь пиринговая качалка файлов. Все содержимое таких носителей оказывается проиндексировано Google и другими поисковиками, поэтому получить доступ к хранящимся на внешних дисках файлам можно по прямой ссылке.

Подсматриваем конфиги

До повальной миграции в облака в качестве удаленных хранилищ рулили простые FTP-серверы, в которых тоже хватало уязвимостей. Многие из них актуальны до сих пор. Например, у популярной программы WS_FTP Professional данные о конфигурации, пользовательских аккаунтах и паролях хранятся в файле ws_ftp.ini . Его просто найти и прочитать, поскольку все записи сохраняются в текстовом формате, а пароли шифруются алгоритмом Triple DES после минимальной обфускации. В большинстве версий достаточно просто отбросить первый байт.

Расшифровать такие пароли легко с помощью утилиты WS_FTP Password Decryptor или бесплатного веб-сервиса .

Говоря о взломе произвольного сайта, обычно подразумевают получение пароля из логов и бэкапов конфигурационных файлов CMS или приложений для электронной коммерции. Если знаешь их типовую структуру, то легко сможешь указать ключевые слова. Строки, подобные встречающимся в ws_ftp.ini , крайне распространены. Например, в Drupal и PrestaShop обязательно есть идентификатор пользователя (UID) и соответствующий ему пароль (pwd), а хранится вся информация в файлах с расширением.inc. Искать их можно следующим образом:

"pwd=" "UID=" ext:inc

Раскрываем пароли от СУБД

В конфигурационных файлах SQL-серверов имена и адреса электронной почты пользователей хранятся в открытом виде, а вместо паролей записаны их хеши MD5. Расшифровать их, строго говоря, невозможно, однако можно найти соответствие среди известных пар хеш - пароль.

До сих пор встречаются СУБД, в которых не используется даже хеширование паролей. Конфигурационные файлы любой из них можно просто посмотреть в браузере.

Intext:DB_PASSWORD filetype:env

С появлением на серверах Windows место конфигурационных файлов отчасти занял реестр. Искать по его веткам можно точно таким же образом, используя reg в качестве типа файла. Например, вот так:

Filetype:reg HKEY_CURRENT_USER "Password"=

Не забываем про очевидное

Иногда добраться до закрытой информации удается с помощью случайно открытых и попавших в поле зрения Google данных. Идеальный вариант - найти список паролей в каком-нибудь распространенном формате. Хранить сведения аккаунтов в текстовом файле, документе Word или электронной таблице Excel могут только отчаянные люди, но как раз их всегда хватает.

Filetype:xls inurl:password

С одной стороны, есть масса средств для предотвращения подобных инцидентов. Необходимо указывать адекватные права доступа в htaccess, патчить CMS, не использовать левые скрипты и закрывать прочие дыры. Существует также файл со списком исключений robots.txt, запрещающий поисковикам индексировать указанные в нем файлы и каталоги. С другой стороны, если структура robots.txt на каком-то сервере отличается от стандартной, то сразу становится видно, что на нем пытаются скрыть.

Список каталогов и файлов на любом сайте предваряется стандартной надписью index of. Поскольку для служебных целей она должна встречаться в заголовке, то имеет смысл ограничить ее поиск оператором intitle . Интересные вещи находятся в каталогах /admin/, /personal/, /etc/ и даже /secret/.

Следим за обновлениями

Актуальность тут крайне важна: старые уязвимости закрывают очень медленно, но Google и его поисковая выдача меняются постоянно. Есть разница даже между фильтром «за последнюю секунду» (&tbs=qdr:s в конце урла запроса) и «в реальном времени» (&tbs=qdr:1).

Временной интервал даты последнего обновления файла у Google тоже указывается неявно. Через графический веб-интерфейс можно выбрать один из типовых периодов (час, день, неделя и так далее) либо задать диапазон дат, но такой способ не годится для автоматизации.

По виду адресной строки можно догадаться только о способе ограничить вывод результатов с помощью конструкции &tbs=qdr: . Буква y после нее задает лимит в один год (&tbs=qdr:y), m показывает результаты за последний месяц, w - за неделю, d - за прошедший день, h - за последний час, n - за минуту, а s - за секунду. Самые свежие результаты, только что ставшие известными Google, находится при помощи фильтра &tbs=qdr:1 .

Если требуется написать хитрый скрипт, то будет полезно знать, что диапазон дат задается в Google в юлианском формате через оператор daterange . Например, вот так можно найти список документов PDF со словом confidential, загруженных c 1 января по 1 июля 2015 года.

Confidential filetype:pdf daterange:2457024-2457205

Диапазон указывается в формате юлианских дат без учета дробной части. Переводить их вручную с григорианского календаря неудобно. Проще воспользоваться конвертером дат .

Таргетируемся и снова фильтруем

Помимо указания дополнительных операторов в поисковом запросе их можно отправлять прямо в теле ссылки. Например, уточнению filetype:pdf соответствует конструкция as_filetype=pdf . Таким образом удобно задавать любые уточнения. Допустим, выдача результатов только из Республики Гондурас задается добавлением в поисковый URL конструкции cr=countryHN , а только из города Бобруйск - gcs=Bobruisk . В разделе для разработчиков можно найти полный список .

Средства автоматизации Google призваны облегчить жизнь, но часто добавляют проблем. Например, по IP пользователя через WHOIS определяется его город. На основании этой информации в Google не только балансируется нагрузка между серверами, но и меняются результаты поисковой выдачи. В зависимости от региона при одном и том же запросе на первую страницу попадут разные результаты, а часть из них может вовсе оказаться скрытой. Почувствовать себя космополитом и искать информацию из любой страны поможет ее двухбуквенный код после директивы gl=country . Например, код Нидерландов - NL, а Ватикану и Северной Корее в Google свой код не положен.

Часто поисковая выдача оказывается замусоренной даже после использования нескольких продвинутых фильтров. В таком случае легко уточнить запрос, добавив к нему несколько слов-исключений (перед каждым из них ставится знак минус). Например, со словом Personal часто употребляются banking , names и tutorial . Поэтому более чистые поисковые результаты покажет не хрестоматийный пример запроса, а уточненный:

Intitle:"Index of /Personal/" -names -tutorial -banking

Пример напоследок

Искушенный хакер отличается тем, что обеспечивает себя всем необходимым самостоятельно. Например, VPN - штука удобная, но либо дорогая, либо временная и с ограничениями. Оформлять подписку для себя одного слишком накладно. Хорошо, что есть групповые подписки, а с помощью Google легко стать частью какой-нибудь группы. Для этого достаточно найти файл конфигурации Cisco VPN, у которого довольно нестандартное расширение PCF и узнаваемый путь: Program Files\Cisco Systems\VPN Client\Profiles . Один запрос, и ты вливаешься, к примеру, в дружный коллектив Боннского университета.

Filetype:pcf vpn OR Group

INFO

Google находит конфигурационные файлы с паролями, но многие из них записаны в зашифрованном виде или заменены хешами. Если видишь строки фиксированной длины, то сразу ищи сервис расшифровки.

Пароли хранятся в зашифрованном виде, но Морис Массар уже написал программу для их расшифровки и предоставляет ее бесплатно через thecampusgeeks.com .

При помощи Google выполняются сотни разных типов атак и тестов на проникновение. Есть множество вариантов, затрагивающих популярные программы, основные форматы баз данных, многочисленные уязвимости PHP, облаков и так далее. Если точно представлять то, что ищешь, это сильно упростит получение нужной информации (особенно той, которую не планировали делать всеобщим достоянием). Не Shodan единый питает интересными идеями, но всякая база проиндексированных сетевых ресурсов!

В Hibernate работа с БД осуществляется через объект типа org.hibernate.Session .
Выдержка из документации:
The main runtime interface between a Java application and Hibernate. This is the central API class abstracting the notion of a persistence service.
The lifecycle of a Session is bounded by the beginning and end of a logical transaction. (Long transactions might span several database transactions.)
The main function of the Session is to offer create, read and delete operations for instances of mapped entity classes.

Интерфейс org.hibernate.Session является мостом между приложением и Hibernate. С помощью сессий выполняются все CRUD-операции с объектами-сущностями . Объект типа Session получают из экземпляра типа org.hibernate.SessionFactory , который должен присутствовать в приложении в виде singleton .
3). Состояния объектов
Объект-сущность может находиться в одном из 3-х состояний (статусов):
  • transient object . Объекты в данном статусе - это заполненные экземпляры классов-сущностей. Могут быть сохранены в БД. Не присоединены к сессии. Поле Id не должно быть заполнено, иначе объект имеет статус detached ;
  • persistent object . Объект в данном статусе - так называемая хранимая сущность, которая присоединена к конкретной сессии. Только в этом статусе объект взаимодействует с базой данных. При работе с объектом данного типа в рамках транзакции все изменения объекта записываются в базу;
  • detached object . Объект в данном статусе - это объект, отсоединённый от сессии, может существовать или не существовать в БД.
Любой объект-сущность можно переводить из одного статуса в другой. Для этого в интерфейсе Session существуют следующие методы:
  • persist(Object) - преобразует объект из transient в persistent , то есть присоединяет к сессии и сохраняет в БД. Однако, если мы присвоим значение полю Id объекта, то получим PersistentObjectException - Hibernate посчитает, что объект detached , т. е. существует в БД. При сохранении метод persist() сразу выполняет insert , не делая select .
  • merge(Object) - преобразует объект из transient или detached в persistent . Если из transient , то работает аналогично persist() (генерирует для объекта новый Id , даже если он задан), если из detached - загружает объект из БД, присоединяет к сессии, а при сохранении выполняет запрос update
  • replicate(Object, ReplicationMode) - преобразует объект из detached в persistent , при этом у объекта обязательно должен быть заранее установлен Id . Данный метод предназначен для сохранения в БД объекта с заданным Id , чего не позволяют сделать persist() и merge() . Если объект с данным Id уже существует в БД, то поведение определяется согласно правилу из перечисления org.hibernate.ReplicationMode :
    ReplicationMode.IGNORE - ничего не меняется в базе.
    ReplicationMode.OVERWRITE - объект сохраняется в базу вместо существующего.
    ReplicationMode.LATEST_VERSION - в базе сохраняется объект с последней версией.
    ReplicationMode.EXCEPTION - генерирует исключение.
  • delete(Object) - удаляет объект из БД, иными словами, преобразует persistent в transient . Object может быть в любом статусе, главное, чтобы был установлен Id .
  • save(Object) - сохраняет объект в БД, генерируя новый Id , даже если он установлен. Object может быть в статусе transient или detached
  • update(Object) - обновляет объект в БД, преобразуя его в persistent (Object в статусе detached )
  • saveOrUpdate(Object) - вызывает save() или update()
  • refresh(Object) - обновляет detached -объект, выполнив select к БД, и преобразует его в persistent
  • get(Object.class, id) - получает из БД объект класса-сущности с определённым Id в статусе persistent
Объект Session кэширует у себя загруженные объекты; при загрузке объекта из БД в первую очередь проверяется кэш. Для того, чтобы удалить объект из кэша и отсоединить от сессии, используется session.evict(Object) . Метод session.clear() применит evict() ко всем объектам в сессии.

А теперь обратим внимание на аннотации @OneToMany и @ManyToOne в классах-сущностях. Параметр fetch в @OneToMany обозначает, когда загружать дочерние объекты. Может иметь одно из двух значений, указанных в перечислении javax.persistence.FetchType :

FetchType.EAGER - загружать коллекцию дочерних объектов сразу же, при загрузке родительских объектов.
FetchType.LAZY - загружать коллекцию дочерних объектов при первом обращении к ней (вызове get ) - так называемая отложенная загрузка.

Параметр cascade обозначает, какие из методов интерфейса Session будут распространяться каскадно к ассоциированным сущностям. Например, в классе-сущности User для коллекции tasks укажем:

@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.MERGE}) public List
Тогда при выполнении session.persist(user) или session.merge(user) операции persist или merge будут применены ко всем объектам из tasks . Аналогично для остальных операций из перечисления javax.persistence.CascadeType . CascadeType.ALL применяет все операции из перечисления. Необходимо правильно настроить CascadeType , дабы не подгружать из базы кучу лишних ассоциированных объектов-сущностей.

4). Извлечение объектов из БД
Приведём простой пример:

@Autowired private SessionFactory sessionFactory public void getTasks(Long userId) { ... Session session = sessionFactory.openSession(); User user = (User) session.load(User.class, userId); Session session = sessionFactory.openSession(); List tasksList = user.getTasks(); ... }
Вместо метода session.get() можно использовать session.load() . Метод session.load() возвращает так называемый proxy-object . Proxy-object - это объект-посредник, через который мы можем взаимодействовать с реальным объектом в БД. Он расширяет функционал объекта-сущности. Взаимодействие с proxy-object полностью аналогично взаимодействию с объектом-сущностью. Proxy-object отличается от объекта-сущности тем, что при создании proxy-object не выполняется ни одного запроса к БД, т. е. Hibernate просто верит нам, что объект с данным Id существует в БД. Однако первый вызванный get или set у proxy-object сразу инициирует запрос select , и если объекта с данным Id нет в базе, то мы получим ObjectNotFoundException . Основное предназначение proxy-object - реализация отложенной загрузки.

Вызов user.getTasks() инициирует загрузку задач юзера из БД, так как в классе User для tasks установлен FetchType.LAZY .

LazyInitializationException
С параметром FetchType.LAZY нужно быть аккуратнее. Иногда при загрузке ассоциированных сущностей мы можем поймать исключение LazyInitializationException . В вышеуказанном коде во время вызова user.getTasks() user должен быть либо в статусе persistent , либо proxy .

Также LazyInitializationException может вызвать небольшое изменение в нашем коде:

Public List getTasks(Long userId) { ... Session session = sessionFactory.openSession(); User user = (User) session.load(User.class, userId); List tasksList = user.getTasks(); session.close(); return tasksList; }
Здесь теоретически всё верно. Но при попытке обращения к tasksList мы МОЖЕМ получить LazyInitializationException . Но в дебагере данный код отрабатывает верно. Почему? Потому, что user.getTasks() только возвращает ссылку на коллекцию, но не ждёт её загрузки. Не подождав, пока загрузятся данные, мы закрыли сессию. Выход - выполнять в транзакции, т. е.:

Public List getTasks(Long userId) { ... User user = (User) session.load(User.class, userId); Session session = sessionFactory.openSession(); session.beginTransaction(); List tasksList = user.getTasks(); session.getTransaction().commit(); return tasksList; }

Выборка с условиями
А теперь приведём несколько простых примеров выборки данных с условиями. Для этого в Hibernate используются объекты типа org.hibernate.Criteria :

Public List getUser(String login) { ... Session session = sessionFactory.openSession(); Criteria userCriteria = session.createCriteria(User.class); userCriteria.add(Restrictions.eq("login", login)); user = (User) userCriteria.uniqueResult(); session.close(); ... }
Здесь понятно, что мы выполняем select * from user where login="login" . В метод add мы передаём объект типа Criterion , представляющий определённый критерий выборки. Класс org.hibernate.criterion.Restrictions предоставляет множество различных видов критериев. Параметр «login» обозначает название свойства класса-сущности, а не поля в таблице БД.
Приведём ещё пару примеров:

А).
public List getTasksByName(String name) { ... session = sessionFactory.openSession(); Criteria criteria = session.createCriteria(Task.class); List tasks = criteria.add(Restrictions.like("name", name, MatchMode.ANYWHERE)).list(); ... }
Здесь мы выбираем по содержимому свойства name класса-сущности Task . MatchMode.ANYWHERE означает, что нужно искать подстроку name в любом месте свойства «name» .

Б).
А здесь мы получаем 50 строк, начиная с 20-го номера в таблице.

Public List getTasks() { ... Session session = sessionFactory.openSession(); Criteria criteria = session.createCriteria(Task.class); List tasks = criteria.setFirstResult(20).setMaxResults(50).list(); ... }

5). Сохранение объектов
Давайте разберём несколько способов сохранения объекта-сущности в базу данных.

А). Создаём transient-object и сохраняем в базу:

@Autowired private UserDao userDao; @Autowired private SessionFactory sessionFactory; public void saveUser(String login) { User user = userDao.getUserByLogin(login); Session session = sessionFactory.openSession(); session.openTransaction(); Task task = new Task(); task.setName("Задача 1"); task.setDefinition("Задача 1"); task.setTaskDate(new Date()); task.setUser(user); session.saveOrUpdate(task); session.flush(); session.getTransaction().commit(); return task.getTaskId(); }
Отметим несколько нюансов. Во-первых, сохранение в БД можно производить только в рамках транзакции. Вызов session.openTransaction() открывает для данной сессии новую транзакцию, а session.getTransaction().commit() её выполняет. Во-вторых, в метод task.setUser(user) мы передаём user в статусе detached . Можно передать и в статусе persistent .

Данный код выполнит (не считая получения user ) 2 запроса - и insert into task...
Вместо saveOrUpdate() можно выполнить save() , persist() , merge() - будет также 2 запроса. Вызов session.flush() применяет все изменения к БД, но, если честно, этот вызов здесь бесполезен, так как ничего не сохраняется в БД до commit() , который сам вызовет flush() .

Помним, что если мы внутри транзакции что-то изменим в загруженном из БД объекте статуса persistent или proxy-object , то выполнится запрос update . Если task должен ссылаться на нового user , то делаем так:

User user = new User(); // Создаём transient-object user.setLogin("user"); user.setPassword("user"); ... task.setUser(user); session.saveOrUpdate(task); // Сохраняем
Внимание: в классе Task для поля user должен быть установлен CascadeType.PERSIST , CascadeType.MERGE или CascadeType.ALL .

Если мы имеем на руках userId существующего в БД юзера, то нам не обязательно загружать объект User из БД, делая лишний select . Так как мы не можем присвоить ID юзера непосредственно свойству класса Task , нам нужно создать объект класса User с единственно заполненными userId . Естественно, это не может быть transient-object , поэтому здесь следует воспользоваться известным нам proxy-объектом .

Public void saveTask(Long userId, Task task) ... task.setUser((User) session.load(User.class, userId)); // Никакого запроса к БД не происходит session.saveOrUpdate(task); ...
б). Добавляем объект в коллекцию дочерних объектов:

Public Long saveUser(String login) { Session session = sessionFactory.openSession(); session.openTransaction(); user = (User) session.load(User.class, userId); Task task = new Task(); task.setName("Имя"); task.setUser(user); user.getTasks().add(task); session.getTransaction().commit(); return user.getUserId(); }
В User для свойства tasks должен стоять CascadeType.ALL . Если стоит CascadeType.MERGE , то после user.getTasks().add(task) выполнить session.merge(user) . Данный код выполнит 3 запроса - select * from user , select nextval("task_task_id_seq") и insert into task

6). Удаление объектов
а). Можно удалить, создав transient-object :

Public void deleteTask(Long taskId) { Session session = sessionFactory.openSession(); session.openTransaction(); Tasks task = new Tasks(); task.setTaskId(taskId); session.delete(task); session.getTransaction().commit(); }
Данный код удалит только task . Однако, если task - объект типа proxy , persistent или detached и в классе Task для поля user действует CascadeType.REMOVE , то из базы удалится также ассоциированный user . Если удалять юзера не нужно, выполнить что? Правильно, task.setUser(null)

Б). Можно удалить и таким способом:

Public void deleteTask(Long userId, Long taskId) { User user = (User) session.load(User.class, userId); user.getTasks().removeIf((Task task) -> { if (task.getTaskId() == taskId) { task.setUser(null); return true; } else return false; }); }
Данный код просто удаляет связь между task и user . Здесь мы применили новомодное лямбда-выражение . Объект task удалится из БД при одном условии - если изменить кое-что в классе-сущности User :

@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) public List getTasks() { return tasks; } public void setTasks(List tasks) { this.tasks = tasks; }
Параметр orphanRemoval = true указывает, что все объекты Task , которые не имеют ссылки на User , должны быть удалены из БД.

7). Декларативное управление транзакциями
Для декларативного управления транзакциями мы будем использовать Spring Framework . Управление транзакциями осуществляется через менеджер транзакций. Вместо вызовов session.openTransaction() и session.commit() используется аннотация @Transactional . В конфигурации приложения должно присутствовать следующее:


Здесь мы определили бин transactionManager , к которому привязан бин sessionFactory . Класс HibernateTransactionManager является реализацией общего интерфейса org.springframework.transaction.PlatformTransactionManager для SessionFactory библиотеки Hibernate. annotation-driven указывает менеджеру транзакций обрабатывать аннотацию @Transactional .

Болтовня ничего не стоит. Покажите мне код. (Linus Torvalds)

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {ObjectNotFoundException.class, ConstraintViolationException.class}) public Long saveTask(Long userId) { Session session = sessionFactory.getCurrentSession(); Tasks task = new Tasks(); task.setName("Задача 1"); task.setDefinition("Задача 1"); task.setTaskDate(new Date()); task.setUser((User) session.load(User.class, userId)); session.saveOrUpdate(task); return task.getTaskId(); }
Аннотация @Transactional указывает, что метод должен выполняться в транзакции. Менеджер транзакций открывает новую транзакцию и создаёт для неё экземпляр Session , который доступен через sessionFactory.getCurrentSession() . Все методы, которые вызываются в методе с данной аннотацией, также имеют доступ к этой транзакции, потому что экземпляр Session является переменной потока (ThreadLocal). Вызов sessionFactory.openSession() откроет совсем другую сессию, которая не связана с транзакцией.

Параметр rollbackFor указывает исключения, при выбросе которых должен быть произведён откат транзакции. Есть обратный параметр - noRollbackFor , указывающий, что все исключения, кроме перечисленных, приводят к откату транзакции.

Параметр propagation самый интересный. Он указывает принцип распространения транзакции. Может принимать любое значение из перечисления org.springframework.transaction.annotation.Propagation . Приведём пример:

@Autowired private SessionFactory sessionFactory; @Autowired private UserDao userDao; @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {ConstraintViolationException.class}) public Long saveTask(Long userId) { Session session = sessionFactory.getCurrentSession(); User user = userDao.getUserByLogin("user1"); Tasks task = new Tasks(); task.setName("Задача 1"); ... task.setUser(user); session.saveOrUpdate(task); return task.getTaskId(); }
Метод UserDao.getUserByLogin() также может быть помечен аннотацией @Transactional . И здесь параметр propagation определит поведение метода UserDao.getUserByLogin() относительно транзакции метода saveTask() :

  • Propagation.REQUIRED - выполняться в существующей транзакции, если она есть, иначе создавать новую.
  • Propagation.MANDATORY - выполняться в существующей транзакции, если она есть, иначе генерировать исключение.
  • Propagation.SUPPORTS - выполняться в существующей транзакции, если она есть, иначе выполняться вне транзакции.
  • Propagation.NOT_SUPPORTED - всегда выполняться вне транзакции. Если есть существующая, то она будет остановлена.
  • Propagation.REQUIRES_NEW - всегда выполняться в новой независимой транзакции. Если есть существующая, то она будет остановлена до окончания выполнения новой транзакции.
  • Propagation.NESTED - если есть текущая транзакция, выполняться в новой, так называемой, вложенной транзакции. Если вложенная транзакция будет отменена, то это не повлияет на внешнюю транзакцию; если будет отменена внешняя транзакция, то будет отменена и вложенная. Если текущей транзакции нет, то просто создаётся новая.
  • Propagation.NEVER - всегда выполнять вне транзакции, при наличии существующей генерировать исключение.
Хороший материал о транзакциях . Следует помнить, что использование транзакций несёт дополнительные издержки в производительности.
Ну что ж, подведём итоги
В моей статье я осветил самые основные принципы работы с сессиями и транзакциями в Hibernate. Надеюсь, что начинающим Java-программистам статья будет полезна при преодолении первого порога в изучении суперклассной (не для всех, возможно) библиотеки Hibernate. Желаю всем успехов в нашей сложной и интересной программерской деятельности!
2.
3.
4.
5. Ожидания
6.
7. WebDriver API
8. Приложение: Часто Задаваемые Вопросы

5. Ожидания

В наши дни большинство веб-приложений используют AJAX технологии. Когда страница загружена в браузере, элементы на этой странице могут подгружаться с различными временными интервалами. Это затрудняет поиск элементов, если элемент не присутствует в DOM , возникает исключение ElementNotVisibleException. Используя ожидания, мы можем решить эту проблему. Ожидание дает некий временной интервал между произведенными действиями - поиске элемента или любой другой операции с элементом.

Selenium WebDriver предоставляет два типа ожиданий - неявное (implicit) и явное (explicit). Явное ожидание заставляет WebDriver ожидать возникновение определенного условия до произведения действий. Неявное ожидание заставляет WebDriver опрашивать DOM определенное количество времени, когда пытается найти элемент.

5.1 Явные ожидания

Явное ожидание - это код, которым вы определяете какое необходимое условие должно произойти для того, чтобы дальнейший код исполнился. Худший пример такого кода - это использование команды time.sleep(), которая устанавливает точное время ожидания. Существуют более удобные методы, которые помогут написать вам код, ожидающий ровно столько, сколько необходимо. WebDriverWait в комбинации с ExpectedCondition является одним из таких способов.

From selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver = webdriver.Firefox() driver.get("http://somedomain/url_that_delays_loading") try: element = WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "myDynamicElement"))) finally: driver.quit()
Этот код будет ждать 10 секунд до того, как отдаст исключение TimeoutException или если найдет элемент за эти 10 секунд, то вернет его. WebDriverWait по умолчанию вызывает ExpectedCondition каждые 500 миллисекунд до тех пор, пока не получит успешный return. Успешный return для ExpectedCondition имеет тип Boolean и возвращает значение true, либо возвращает not null для всех других ExpectedCondition типов.

Ожидаемые условия
Существуют некие условия, которые часто встречаются при автоматизации веб-сайтов. Ниже перечислены реализации каждого. Связки в Selenium Python предоставляют некоторые удобные методы, так что вам не придется писать класс expected_condition самостоятельно или же создавать собственный пакет утилит.

  • title_is
  • title_contains
  • presence_of_element_located
  • visibility_of_element_located
  • visibility_of
  • presence_of_all_elements_located
  • text_to_be_present_in_element
  • text_to_be_present_in_element_value
  • frame_to_be_available_and_switch_to_it
  • invisibility_of_element_located
  • element_to_be_clickable - it is Displayed and Enabled.
  • staleness_of
  • element_to_be_selected
  • element_located_to_be_selected
  • element_selection_state_to_be
  • element_located_selection_state_to_be
  • alert_is_present
from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 10) element = wait.until(EC.element_to_be_clickable((By.ID,"someid")))
Модуль expected_conditions уже содержит набор предопределенных условий для работы с WebDriverWait.

5.2 Неявные ожидания

Неявное ожидание указывает WebDriver"у опрашивать DOM определенное количество времени, когда пытается найти элемент или элементы, которые недоступны в тот момент. Значение по умолчанию равно 0. После установки, неявное ожидание устанавливается для жизни экземпляра WebDriver объекта.

From selenium import webdriver driver = webdriver.Firefox() driver.implicitly_wait(10) # seconds driver.get("http://somedomain/url_that_delays_loading") myDynamicElement = driver.find_element_by_id("myDynamicElement")
Перейти к следующей главе.

  • Блог компании Фонд развития интернет-инициатив
    • Перевод

    User story является неотъемлемой частью гибких методик разработки. С помощью нее можно разбить свою работу на серию задач, выполнение каждой из которых привносит ощутимую ценность в процесс реализации проекта. Все эти задачи можно обсуждать и распределять по важности независимо друг от друга.

    User story или «пользовательские истории/пожелания пользователя» являются методикой, опирающейся на другие практики agile, включая принципы непрерывной поставки и непосредственного общения с пользователями. Недостаточно просто понять, каким будет ваш пользователь; реальный пользователь вашей системы должен находиться рядом с командой на протяжении длительного времени.


    User story вкратце описывает:

    • человека, использующего систему (заказчик);
    • то, что должно содержаться в данной системе (примечание);
    • то, для чего она нужна пользователю (цель).
    User story может быть добавлена к бэклогу проекта на любом этапе спринта любым членом команды. Владелец проекта решает, как координировать и приоритезировать пожелания пользователя, и подбирает их вначале каждого цикла спринта.

    Обсудите историю перед планированием этапов с:

    • соответствующими членами команды;
    • экспертами по данному вопросу;
    • заинтересованными сторонами.

    Карточки пожеланий пользователя

    Пожелание пользователя представляет собой карточку с заголовком и несколькими строчками текста. Ваши карточки могут быть как виртуальными, так и материальными. При разработке крупного продукта/системы храните их в цифровом формате, а затем оформляйте материальные карточки в рамках планирования спринта.

    При оформлении карточки с пожеланием пользователя убедитесь, что оно (пожелание) хорошо сформулировано. Не пропускайте часть, в которой объясняется необходимость создания данного элемента системы только потому, что это может вызвать у вас трудности.

    Вы можете выработать список критериев приемки при выполнении пользовательских требований к системе. Он должен стать напоминанием к тому, чтобы протестировать или проверить определенные вещи, которые могут быть затронуты при обсуждении. Но не нужно ориентироваться на него при определении объемов работ на основании user story.

    Если пожелание слишком объемное, разбейте его на более мелкие части, так как при таком подходе возрастает шанс, что в результате получится готовый к последующему использованию код.

    Структура

    Карточка составляется по стандартной схеме:
    • заголовок;
    • заказчик (actor, актер);
    • примечание;
    • цель.
    Она не отражает каждую мелочь, но вы должны детально обсудить пользовательскую историю в подходящее для команды время.

    Движение «от цели»
    Создание полезных программных систем трудоёмко, поэтому вы должны быть уверены, что решаете правильную проблему. В гибких методологиях применяют подход «от обратного» – что пытается получить пользователь системы на выходе? Если вы углубитесь в решение данного вопроса без достаточного понимания ваших пользователей, вы рискуете решить не ту проблему или найти решение, которое на самом деле не подходит вашим пользователям в реальной жизни. Поэтому самой важной частью user story является её цель.
    Цель
    Понимание целей поможет вам при решении вопроса о том, выполнили ли вы требования пользователя, иными словами, соответствует ли проделанная вами работа цели, стоящей перед пользователем?

    При написании user story с вашей командой разработчиков всегда начинайте с обдумывания и обсуждения целей вашего пользователя:

    • почему он хочет использовать эту систему?
    • чего он пытается достичь?
    • что заставило его искать ваш сервис?
    • в каких условиях он использует её: дома/на работе/по телефону/во время ухода за ребенком?
    • как часто он пользуется ей?
    Сюзанна и Джеймс Робертсон дают отличный совет по этому поводу в третьем издании книги «Mastering the Requirements Process».
    Заказчик (актер)
    Подробное описание заказчика (актера) поможет вам разбить процесс построения взаимодействия на логически связанные отрезки.
    Иногда заказчик будет пользователем вашей системы, в других случаях – администратором, техником или менеджером вашей организации.
    Убедитесь, что вы достаточно хорошо знаете своих пользователей на основании уже проделанной работы или проведенных ранее исследований. Если это не так, найдите время расширить свое понимание пользователей.
    Примечания
    Используйте их как блок передачи информации об основом типе взаимодействия, которое должно рассматриваться как часть пользовательского опыта. Помните, что карточка не должна отражать каждую деталь этого взаимодействия.
    Общение с пользователем напрямую
    Agile-команды предпочитают общение лицом к лицу подробной документации.

    Разговор лицом к лицу:

    • быстрее;
    • точнее, чем написанная документация;
    • позволяет разработчикам выстроить детальную модель целей пользователя, рабочих процессов, ограничений и многих вопросов, которые должны быть приняты во внимание при разработке ПО.
    Карточка – это всего лишь шаблон, гарантия, что разговор состоится в подходящее время. Воспользуйтесь карточками и несколькими вариантами для завязки разговора, чтобы оценить, сколько времени вам понадобится, чтобы завершить работу над составлением user story и поместить её в бэклог проекта.

    Когда начнется непосредственно разработка, пообщайтесь с пользователями или их представителями на предмет уточнения деталей требований. User story – это обобщение всех разговоров, набросков и диаграмм – не просто карточка. Необязательно записывать и хранить все разговоры, но нужно правильно интерпретировать их в автоматизированные тесты или работающий код.

    Использование пользовательских историй на этом этапе позволит избежать «информационной перегрузки» – болезненного состояния, при котором пытаешься угадать детали какой-либо цели из далекого будущего.

    Критерии приемки для user story

    Критерии приемки помогают, в частности, понять, выполнено ли пожелание пользователя. Они сопровождают общение между разработчиком и пользователем или его представителем и оцениваются до начала разработки. Фиксируйте их на оборотной части карточки только в том случае, если, по мнению команды, они содержат соображения пользователя, о которых в дальнейшем могут забыть. Иногда написание критериев приемки на карточке полезно, если пользователь или представитель пользователя оказывается недоступен, и его некому подменить для проведения личной встречи.

    Истории «работают» только для agile-команд

    Успех user story зависит от регулярного общения напрямую между разработчиками и пользователями или его представителями. Ваш сервис-менеджер и другие представители пользователя должны быть доступны для общения с разработчикам каждый день, и иметь достаточно времени, чтобы обдумать ваши вопросы и ответить на них. Не стоит недооценивать то, как много времени может отнимать эта работа!

    Откуда берутся пожелания пользователя

    Пожелания могут поступать из многих мест, но наиболее распространённые источники включают в себя:
    1. Воркшопы по написанию user story (чаще всего происходят на начальном этапе проекта); команда разработчиков и заинтересованные стороны собираются, чтобы обозначить пожелания пользователей.
    2. Интервью с реальными пользователями – в идеале, вы должны найти несколько пользователей, к которым команда разработчиков будет иметь постоянный доступ.
    3. Представители пользователя в составе вашей команды – это может быть сервис-менеджер или владелец продукта.
    4. Наблюдение – посмотрите, как реальные пользователи работают в вашей системе.
    Мы обсудили данный материал с рядом стартапов 4-го набора Акселератора ФРИИ [ближайший День открытых дверей Акселератора состоится в следующий четверг 12-го февраля 2015 г. ]:

    1. Используете ли вы user story? К каким источникам вы обращаетесь при составлении user story?

    Писали еще до проектирования новой версии сайта и сделали, как мне кажется, одну большую ошибку. Не пообщались с достаточным количеством потенциальных пользователей до создания историй. В итоге получили истории исходя из своих предположений, которые разнились с реальными потребностями и могли увести проект не в ту сторону.


    Cкорее нет, чем да. Мы сначала разработали продукт и уже потом общались с пользователями. Да, мы вносили определённые изменения в контент, основываясь на отзывах, но никогда не ставили себе целью полностью основывать «таски» на user story.


    Термин user story мы никогда не использовали, но по факту отвечали на вопросы «Кто наш клиент?», «Как он использует наш сервис?» и «Почему ему нужен наш сервис?». Информацию получали из заявок клиентов, которые поступают к нам на сайт, и из личного общения с теми клиентами, кто заказал и кто не заказал наши услуги (услуги переводчиков).


    Александр Грицай, CEO, Forecast NOW!

    Мы используем методику выявления потребностей клиентов с помощью обратной связи, опросов, живого общения (у нас непростой B2B-продукт, и число клиентов измеряется десятками), ранжируем их, выделяем на спринт приоритетные по критериям полезности для текущих и будущих пользователей, раз в месяц выпускаем обновление.


    Ольга Книга, со-основатель и CEO, Merku.ru

    Мы используем их, как и любой инструмент - тогда, когда это необходимо. В первую очередь, при проектировании посадочной страницы и новых функций продукта.


    Согласны ли вы с утверждением, что методика user story подходит только для agile-команд?

    Александр Богомолов, экс-проектный менеджер, ПоискСтроек

    Не очень понял вопроса – чем мешает создание историй любой команде? Да – в проектах с жестким ТЗ истории не очень нужны на первый взгляд, но они помогают превратить такую разработку в более осмысленную. Помогают понять, зачем вообще реализуется та или иная функция.


    Леонид Тощев, технический директор, Datamonkey

    Сейчас понятие agile настолько размылось– мне кажется, нет таких компаний, которые не agile. Соответственно, user story могут использоваться всеми.


    Мария Теркина, со-основатель и CEO, Глоберлэнд

    Думаю, user story вписывается в agile-подход, но может использоваться и в других подходах к разработке.


    Мария Подоляк, со-основатель Gagarin Labs

    Подход к проектированию функционала продукта или интерфейса при помощи user stories – это же изначально не разработка Agile. Так называемые use cases (юс кейсы, примеры использования) существуют давно в практике разработки технических заданий. Подход такой же, немного был видоизменен под нужды Agile.

    В частных разговорах двух менеджеров продуктов можно услышать: «О, мы начали делать новый продукт для распознавания цвета кошек на фотографиях!» В ответ можно услышать: «Давай юс кейс». Мы разговариваем пользовательскими историями, ведь только так можно понять, какая «боль пользователя» решается, какой инновационный или кардинально иной способ решения проблемы используется, как это происходит на уровне функционала. История помогает «примерить» решение на себя.

    User stories используются и при проектировании интерфейсов. С этим подходом я познакомилась в Школе проектирования интерфейсов. Мы рисовали портрет пользователя (еще их иногда персонами называют), писали сценарий истории, дальше мы вырезали из бумаги интерфейс для того, чтобы разыграть сценарий или историю (так происходило тестирование гипотезы).

    В ноябре я попробовала применить такой же подход для проектирования контент-стратегии компании или продукта на семинаре в Школе интернет-маркетинга в Нижнем Новгороде. И прекрасно получилось. Так что подход применим ко всему в продукте: функционалу, интерфейсу, маркетингу и т.д. Заставляет думать «головой пользователя», понимать его проблему, а не опускаться в галлюцинации.
    Так что Agile ничего нового не сделал, просто адаптировал существующий подход под свои нужды. Добавить метки

    У всех, кто впервые сталкивается с MODx Evolution, очень часто возникают подобные вопросы: как вывести дату создания документа, как вывести автора документа, как вывести заголовок родителя и т.д. и т.п. Согласитесь, не для всех это может оказаться элементарным на первых шагах изучения MODx. А сколько было потрачено нервов и времени в бесплодных попытках найти нужный ответ! И не всегда даже найденный ответ бывает очевиден и понятен. Хватит! Настало время дать сразу все ответы на все вопросы. Ну в меру собственных сил и знаний, конечно. В этой статье я буду собирать все подобные вопросы и стараться дать максимально подробный ответ, конечно же все это будет сопровождаться готовыми рабочими примерами, которые каждый сможет применить на практике.

    Автор не считает себя великим гуру, поэтому, в подготовке этой статьи рассчитывает на помощь читателей. Присылайте свои вопросы и найденные решения, комментируйте и предлагайте альтернативные подходы, критикуйте и становитесь соавторами этой подборки.

    Спрашивали? Отвечаем!

    Сниппеты можно вызывать двумя способами:

    [[НазваниеСниппета]] - кэшируемый вызов сниппета
    [!НазваниеСниппета!] - некэшируемый вызов сниппета

    Если вызов основного сниппета некэшируемый, то в шаблоне сниппет обязательно должен быть кэшируемым. Более того, чтобы избежать проблем с выводом сниппетов (например с постраничным разбиением), основной сниппет желательно всегда делать некэшируемым.

    К примеру, имеем некэшируемый вызов Ditto :

    [!Ditto? &startID=`10` &tpl=`ditto_tpl`!]

    у которого в шаблоне ditto_tpl надо вызвать сниппет Wayfinder :


    [`]]

    Расширим задачу, сформулировав вопрос так: Как вообще выводить даты? Т.е. это может быть любая дата, как в шаблоне для Ditto так и на странице самого документа.

    а) Задача: вывести дату создания документа в ленте новостей

    Для вывода даты нам потребуется вставить в шаблон news_tpl в месте вывода этой даты специальный плэйсхолдер Ditto - [+date+] - он выводит дату в установленном формате (которую мы зададим позже, при вызове Ditto). По умолчанию используется значение createdon (дата создания документа). Может принимать значения: editedon (дата последнего редактирования) и pub_date (дата публикации документа).

    Создаем чанк news_tpl :


    [+date+] | [+longtitle+]

    [+introtext+]


    Для того, чтобы задать параметр из которого Ditto будет брать значение даты используем &dateSource=`editedon` . Для того чтобы задать формат даты, используем &dateFormat=`%d.%m.%Y` , где значением выступает любой валидный формат времени, который соответствует правилам функции PHP - strftime:

    Символы для обозначения формата времени функции strftime
    %a сокращенное название дня недели в текущей локали Пн
    %A полное название дня недели в текущей локали 18.03.2019
    %b сокращенное название месяца недели в текущей локали 18.03.2019
    %B полное название месяца недели в текущей локали Март
    %c предпочтительный формат даты и времени в текущей локали 18.03.2019
    %C столетие (год, деленный на 100 и огругленный до целого, от 00 до 99) 18.03.2019
    %d день месяца в виде десятичного числа (от 01 до 31) 18.03.2019
    %D аналогично %m/%d/%y 18.03.2019
    %e день месяца в виде десятичного числа, если это одна цифра, то перед ней добавляется пробел (от " 1" до "31") 18.03.2019
    %g подобно %G, но без столетия. 18.03.2019
    %G Год, 4-значное число, соответствующее номеру недели по ISO (см. %V). Аналогично %Y, за исключением того, что если номер недели по ISO соответствует предыдущему или следующему году, используется соответствующий год. 2019
    %h аналогично %b мар
    %H номер часа от 00 до 23 18.03.2019
    %I номер часа от 01 до 12 18.03.2019
    %j номер дня в году (от 001 до 366) 077
    %m номер месяца (от 01 до 12) 18.03.2019
    %M минуты 18.03.2019
    %n символ "\n" 18.03.2019
    %p `am" или `pm", или соответствующие строки в текущей локали 18.03.2019
    %r время в формате a.m. или p.m. 18.03.2019
    %R время в 24-часовом формате 18.03.2019
    %S секунды 18.03.2019
    %t символ табуляции ("\t") 18.03.2019
    %T текущее время, аналогично %H:%M:%S 18.03.2019
    %u номер дня недели от 1 до 7, где 1 соответствует понедельнику 18.03.2019
    %U порядковый номер недели в текущем году. Первым днем первой недели в году считается первое воскресенье года. 18.03.2019
    %V Порядковый номер недели в году по стандарту ISO 8601:1988 от 01 до 53, где 1 соответствует первой неделе в году, в которой как минимум 4 дня принадлежат этому году. Первым днем недели считается понедельник. (Используйте %G or %g для определения соответствующего года) 12
    %W порядковый номер недели в текущем году. Первым днем первой недели в году считается первый понедельник года. 18.03.2019
    %w номер дня недели, 0 соответствует воскресенью 18.03.2019
    %x предпочтительный формат даты без времени в текущей локали 18.03.2019
    %X предпочтительный формат времени без даты в текущей локали 18.03.2019
    %y год без столетия (от 00 до 99) 18.03.2019
    %Y год, включая столетие 18.03.2019
    %Z временная зона в виде смещения, аббривеатуры или полного наименования 18.03.2019
    %% символ `%" 18.03.2019

    Примеры вызова Ditto с разными значениями даты:

    [!Ditto? &startID=`10` &tpl=`news_tpl` &dateSource=`createdon` &dateFormat=`%d.%m.%Y` &display=`5`!] // дата создания документа
    [!Ditto? &startID=`10` &tpl=`news_tpl` &dateSource=`editedon` &dateFormat=`%d.%m.%Y` &display=`5`!] // дата последного редактирования документа
    [!Ditto? &startID=`10` &tpl=`news_tpl` &dateSource=`pub_date` &dateFormat=`%d.%m.%Y` &display=`5`!] // дата публикации документа

    б) Задача: вывести дату на странице самой новости

    Казалось бы, чего проще? Меняем в чанке news_tpl все + на * и нужный чанк готов. Но в MODx нет специального тега [*date*], поэтому, вместо него придется использовать [*createdon*] , [*editedon*] или [*pub_date*] .

    Для начала пробуем [*createdon*] или [*editedon*] и получаем вместо даты что-то типа этого:

    1318407717

    Почему же так получилось? Потому что, любое время в БД записывается в виде Unix Timestamp — количество секунд от 1 января 1970 года до текущего момента. Зачем же такое придумали? Ну, например, для того, чтобы была возможность оперировать датой в независимости от ее формата. На самом деле, это будет легко перевести в нужный нам формат, но об этом чуть позже.

    Берем следующий параметр [*pub_date*] и получаем:

    Ну а тут то что не так, спросите вы? Оказывается, по-умолчанию параметр pub_date не устанавливается, поэтому его значение равно 0 и попытка его вывести выдаст 0 или 01.01.1970. Чтобы избежать этого, придется параметр pub_date делать обязательным к заполнению или установить ему значение по умолчанию. В этом нам поможет ManagerManager . Но даже если параметр будет заполнен, на выходе мы вновь увидим количество секунд, прошедших с 01.01.1970.

    Решение 1. На помощь придет PHx , только прежде чем его устанавливать, ознакомьтесь с возможными проблемами. При установленном PHx вывод даты можно сделать таким образом:

    [*createdon:date=`%d.%m.%Y`*] // дата создания документа
    [*editedon:date=`%d.%m.%Y`*] // дата последнего редактирования документа
    [*pub_date:date=`%d.%m.%Y`*] // дата публикации документа

    Кстати, PHx можно использовать и в шаблоне Ditto, вставив вместо плэйсхолдера [+date+] один из этих плэйсхолдеров:

    [+createdon:date=`%d.%m.%Y`+] // дата создания документа
    [+editedon:date=`%d.%m.%Y`+] // дата последнего редактирования документа
    [+pub_date:date=`%d.%m.%Y`+] // дата публикации документа

    В этом слачае, при вызове Ditto параметры &dateSource и &dateFormat не нужны.

    Решение 2. Для вывода даты в нужном формате можно воспользоваться сниппетом. Создаем новый сниппет, назовем его, к примеру, DateFormat , и помещаем в него следующий код:

    setlocale(LC_ALL, "ru_RU.UTF-8");

    if ($val == "") $val=time();
    if ($format == "") $format = "%d.%m.%Y";
    return strftime($format, $val);
    ?>

    В том месте, где нам необходимо вывести дату, помещаем такой вызов этого сниппета:

    [!DateFormat? &val=`[*createdon*]` &format=`%d.%m.%Y`!]

    В параметре &val мы задаем значение для даты, а в параметре &format нужный формат. Сниппет может применяться как на странице новости, так и в шаблоне для Dito, не забудьте, если Ditto вызывается некэшируемым, то в его шаблоне сниппет должен вызываться как кэшируемый.

    Если вызвать сниппет вообще без параметров:

    [!DateFormat!]

    то он выведет текущую дату в формате заданном по умолчанию: "%d.%m.%Y", этот формат можно поменять в коде сниппета.

    Вот пример, где это может пригодиться: Необходимо отсортировать горящие туры по дате вылета. Дата вылета задается в TV-параметре data_toura с типом ввода Date. При этом, эта дата должна быть выведена в ленте горящих туров и на странице самого тура в формате %d-%m-%Y.

    Для того, чтобы можно было отсортировать туры в ленте горящих туров, которая выводится с помощью снипета Ditto, мы будем использовать параметр &orderBy=`data_toura ASC` . Для того, чтобы сортировка у нас получилась правильная, необходимо задать параметру data_toura значение в формате количества секунд, прошедших с 1 января 1970 года. Для этого нам необходимо установить у этого параметра значение Визуальный компонент как Unixtime :

    Для отображения этой даты в шаблоне Ditto и на странице тура будем использовать PHx или сниппет DateFormat из предыдущей статьи:

    [!DateFormat? &val=`[*data_toura*]` &format=`%d-%m-%Y`!] // на странице тура
    [` &format=`%d-%m-%Y`]] // в шаблоне Ditto

    Создаём сниппет convertDate и вставляем в него такой код:

    $MyDate= (isset($MyDate)) ? $MyDate: $modx -> documentObject["MyDate"];
    $type= (isset($type)) ? $type: $modx -> documentObject["type"];
    $monthes = array("","января","февраля","марта","апреля","мая","июня","июля","августа","сентября","октября","ноября","декабря");
    $day = date("j" ,$MyDate);
    $month = $monthes;
    $year = date("Y",$MyDate);

    echo $day." ".$month." ".$year." года";
    ?>

    Вызываем сниппет:

    [!convertDate? &MyDate=`[*createdon*]`!] // в документе MODx
    [`]] // в шаблоне Ditto

    В качестве значения параметра &MyDate могут выступать createdon, editedon, pub_date, unpub_date и TV-параметр с типом ввода Date и визуальным компонентом Unixtime .

    Решение 1. Для этой цели можно воспользоваться возможностями PHx , если он у вас установлен.

    Логин того, кто создал документ, будет выводиться таким образом:

    [*createdby:userinfo=`username`*] // в докуенте MODx
    [+createdby:userinfo=`username`+] // в шаблоне Ditto

    К примеру, если документ создал администратор, то выведется его логин admin.

    Но для того, чтобы вывести не логин, а полное имя пользователя, которое указано в соответствующем поле в настройках пользователей, делаем так:

    [*createdby:userinfo=`fullname`*]

    В этом случае выведется Default admin account - это значение полного имени администратора по умолчанию. Чтобы вывелось Администратор или как-то иначе, необходимо заменить его полное имя в Пользователи >> Управление менеджерами .

    Для вывода E-mail пользователя:

    [*createdby:userinfo=`email`*]

    Решение 2. Плагин PHx можно заменить простым сниппетом. Создадим новый сниппет UserInfo с таким кодом:

    /*
    * UserInfo - сниппет для получения данных пользователя
    */
    $output = "";
    $userinfo = $modx->getUserInfo($id); //Используем Id пользователя для получения массива с данными пользователя
    $out1 = $userinfo["fullname"]; // Получаем полное имя пользователя
    $out2 = $userinfo["phone"]; // Получаем телефон пользователя
    $out3 = $userinfo["email"]; // Получаем электронный адрес пользователя
    $output = "Автор: " . $out1 . " Телефон: " . $out2 . " E-mail: " . $out3;
    return $output;
    ?>

    В шаблоне помещаем такой вызов сниппета:

    [!UserInfo? &id=`[*createdby*]`!]

    &id=`[*createdby*]` - передаем в наш сниппет ID пользователя, создавшего документ. С помощью параметра editedby можно передать ID пользователя, редактировавшего документ.

    С помощью этого сниппета можно выводить любые данные пользователя из таблицы user_attributes. Описание getUserInfo .

    Решение 1. Вновь расширим задачу: Как вообще выводить параметры родительского документа?

    Вывести ID родительского документа мы можем с помощью специального тега MODx:

    [*parent*] // в документе MODx
    [+parent+] // а шаблоне Ditto

    Чтобы вывести заголовок родительского документа, применяем PHx:

    [*id:parent=`pagetitle`*] // в документе MODx
    [+id:parent=`pagetitle`+] // а шаблоне Ditto

    Чтобы вывести ID родителя родителя, т.е. дедушки:

    [*parent:parent=`id`*] // в документе MODx
    [+parent:parent=`id`+] // а шаблоне Ditto

    Чтобы вывести заголовок прадедушки:

    [*parent:parent=`id`:parent=`pagetitle`*] // в документе MODx
    [+parent:parent=`id`:parent=`pagetitle`+] // а шаблоне Ditto

    [*parent:parent=`id`:parent=`id`:parent=`pagetitle`*] // в документе MODx
    [+parent:parent=`id`:parent=`id`:parent=`pagetitle`+] // а шаблоне Ditto

    Таким образом можно получать не только id или заголовок, а и любой другой параметр, например расширенный заголовок родителя:

    [*id:parent=`longtitle`*] // в документе MODx
    [+id:parent=`longtitle`+] // а шаблоне Ditto

    Решение 2. Если по каким-то причинам вы не можете использовать PHx, на помощь придет сниппет getField .

    Скачайте и установите сниппет, как это описано в документации . Чтобы получить заголовок родительского документа, сниппет надо вызвать с такими параметрами:

    [!GetField? &parent=`0` &field=`pagetitle`!]

    При parent=0 будет использоваться родительский документ. В качестве значения параметра field можно указывать любое поле документа или TV-параметр.

    Чтобы получить заголовок или другое поле "дедушки", используются следующие параметры:

    [!GetField? &parent=`1` &parentLevel=`2` &field=`pagetitle`!]

    При parent=1 используется корневой каталог. Параметр parentLevel определяет глубину вложенности от текущего документа до корневого каталога: 0 - корневой документ, 1 - родитель, 2 - дедушка, 3 - прадедушка и т.д.

    Этот сниппет позволяет выводить любой параметр любого документа. С помощью параметра docid можно задать ID интересующего документа, а с помощью параметра field необходимое поле или TV-параметр:

    [!GetField? &docid=`25` &field=`pagetitle`!]

    К примеру, выводим ленту новостей и необходимо, чтобы длина заголовка не превышала 200 символов, при этом, надо чтобы слова не обрезались и в конце строки появлялись три точки.

    Для этих целей используем сниппет truncate . Код сниппета:

    $lenf = $len;

    //Заменяет символы перевода строки на HTML тег
    $order = array("\r\n", "\n", "\r");
    $replace = "
    ";
    $what = str_replace($order, $replace, $text);

    if (strlen($what) > $lenf) {
    $what = preg_replace("/^(.{" . $lenf . ",}?).*$/is", "$1", $what) . "...";
    }
    return $what;
    ?>

    Вызов сниппета будет таким:

    [!truncate? &text=[*pagetitle*] &len=200!] // в документе MODx
    [ &len=200]] // а шаблоне Ditto

    Сниппет обрежет текст до определенного количества символов не обрезая слова и добавит в конце три точки.

    Придется отредактировать файл assets/snippets/ditto/formats/rss.format.inc.php
    На 80 строчке есть такая запись:

    $GLOBALS["dateSource"] = isset($dateSource) ? $dateSource: "createdon";
    // date type to display (values can be createdon, pub_date, editedon)

    // set tpl rss placeholders
    $placeholders["rss_date"] = array($GLOBALS["dateSource"],"rss_date");

    Т.е. по умолчанию используется createdon . Надо заменить на pub_date , т.е.:

    $GLOBALS["dateSource"] = isset($dateSource) ? $dateSource: "pub_date";

    Если у вас псевдонимы генерируются автоматически с помощью плагина TransAlias, то такие знаки препинания, как "!", ":", "," и т.д. автоматически появятся и в вашем псевдониме, если они присутствуют в заголовке. Но если запятая или восклицательный знак не влияют на работоспособность псевдонима, тем не менее, их присутствие вряд ли его украсит. К счастью, это легко исправляется.

    В настройках TransAlias достаточно выбрать в параметре Restrict alias to значение lowercase alphanumeric и псевдонимы будут генерироваться правильно, т.е. без лишних знаков препинания.

    Для этого воспользуемся сниппетом ChildCounter (сниппет найден ):

    $docid = isset($docid) ? intval($docid) : $modx->documentIdentifier;
    $depth = isset($depth) ? intval($depth) : 0;
    $isfolder = isset($isfolder) ? intval($isfolder): 0;
    $tpl = isset($tpl) ? $tpl: -1;
    $published = isset($published) ? intval($published): 1;
    $davailable = $modx->getChildIds($docid, $depth);

    if ($davailable){
    $where = ($tpl > 0) ? "template=".$tpl: "isfolder=".$isfolder;
    $dcount = $modx->getDocuments($davailable, $published, 0, "id", $where);
    return count($dcount);
    }
    return 0;
    ?>

    Параметры сниппета ChildCounter :

    &dicId - ID сканируемой папки

    &depth - глубина сканирования
    &isfolder - Если 1 - вернёт количество папок, если 0 - количество документов НЕ папок. Значения 0 или 1. По умолчанию 0.
    &published - Если 0 - вернёт количество неопубликованных документов, если 1 - количество опубликованных документов. Значения 0 или 1. По умолчанию 1.

    &tpl - если указан, то возвращает количество документов с шаблоном id которого равен &tpl

    Пример использования:

    []

    AlterTitle выводит pagetitle если не заполнен longtitle .

    Пример использования:

    [`]]

    &id=`[*id*]` - передаем в сниппет ID ресурса, в котором вызываем сниппет. Фактически, этот сниппет можно использовать и для вывода заголовка родителя, передав [*parent*].

    Пример 2.

    Аналогичную конструкцию можем применять и для других параметров. Например, у нас есть параметр foto у шаблона Новости , в котором содержится картинка для новостей, но не у всех новостей будут картинки. И если у новости нет картинки, нам необходимо вывести картинку "заглушку". Я поступил следующим образом, создал еще один параметр nofoto тип ввода Image и назначил его шаблону Новости . В значении по умолчанию указал путь к картинке "заглушке". С помощью плагина ManagerManager спрятал этот параметр от всех пользователей:

    Mm_hideFields("nofoto"); где

    &directOutput=`1` - выводим результат работы сниппета напрямую, без использования плэйсхолдеров

    После встречи очередного нового года и длинных и запойных выходных, вы возвращаетесь к работе и обнаруживаете, что у всех ваших сайтов надо обновлять год в копирайте. А почему бы не автоматизировать сей процесс? Подобная идея уже приходила в голову не только мне, поэтому я довольно быстро нашел готовое решение в пару строчек и немного его модифицировал.

    Создайте новый сниппет (название не так важно, ну пусть будет copyright ) с таким кодом:

    $copyfrom = (isset($copyfrom)) ? $copyfrom: "2012";
    $copyholder = (isset($copyholder)) ? $copyholder: " [(site_name)]";
    if (date("Y") != $copyfrom) { $copytill = " - " . date("Y"); }
    $copyright = " " . $copyfrom . $copytill . $copyholder;
    return $copyright;
    ?>

    Пример вызова:

    []

    []

    ©holder=`Иван Иванович Иванов.` - обладатель авторских прав. По умолчанию - название сайта.