Робот помощник в семье своими руками. Введение


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")
Перейти к следующей главе.

В 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. Желаю всем успехов в нашей сложной и интересной программерской деятельности!

У всех, кто впервые сталкивается с 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=`Иван Иванович Иванов.` - обладатель авторских прав. По умолчанию - название сайта.

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

Что это?

Авторизация - это процесс предоставления доступа к контроллерам и методам действий для определенных пользователей, как правило, находящихся в определенных ролях (например, допуск к админке должны иметь только администраторы).

Зачем нужно использовать?

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

Как использовать в рамках MVC?

Добавление поддержки ролей

ASP.NET Identity содержит строго типизированный базовый класс для доступа и управления ролями, который называется RoleManager , где T является реализацией интерфейса IRole , описывающего механизм хранения данных, используемых для представления ролей. Entity Framework использует класс IdentityRole , являющийся реализацией интерфейса IRole и содержит следующие свойства:

Мы не будем использовать напрямую объекты IdentityRole в нашем приложении, вместо этого добавьте файл класса AppRole.cs в папку Models со следующим содержимым:

Using Microsoft.AspNet.Identity.EntityFramework; namespace Users.Models { public class AppRole: IdentityRole { public AppRole() : base() { } public AppRole(string name) : base(name) { } } }

Класс RoleManager работает с экземплярами IRole с помощью методов и свойств, перечисленных в таблице ниже:

Эти базовые методы реализуют тот же базовый шаблон, который использует класс UserManager для управления пользователями. Добавьте файл AppRoleManager.cs в папку Infrastructure со следующим содержимым:

Using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using System; using Users.Models; namespace Users.Infrastructure { public class AppRoleManager: RoleManager, IDisposable { public AppRoleManager(RoleStore store) : base(store) { } public static AppRoleManager Create(IdentityFactoryOptions options, IOwinContext context) { return new AppRoleManager(new RoleStore(context.Get())); } } }

Этот класс определяет статический метод Create(), который позволит OWIN создавать экземпляры класса AppRoleManager для всех запросов, где требуются данные Identity, не раскрывая информации о том, как данные о ролях хранятся в приложении. Чтобы зарегистрировать класс управления ролями в OWIN, необходимо отредактировать файл IdentityConfig.cs, как показано в примере ниже:

// ... namespace Users { public class IdentityConfig { public void Configuration(IAppBuilder app) { // ... app.CreatePerOwinContext(AppRoleManager.Create); // ... } } }

Это гарантирует, что экземпляры класса AppRoleManager используют тот же контекст базы данных Entity Framework, что и экземпляры AppUserManager.

Создание и удаление ролей

Мы подготовили базовую инфраструктуру для работы с ролями, давайте теперь создадим средство администрирования для работы с ролями. Сначала давайте определим методы действия и представления для управления ролями. Добавьте контроллер RoleAdmin в проект приложения с кодом, показанным в примере ниже:

Using System.Web; using System.Web.Mvc; using System.Threading.Tasks; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using System.ComponentModel.DataAnnotations; using Users.Infrastructure; using Users.Models; namespace Users.Controllers { public class RoleAdminController: Controller { private AppUserManager UserManager { get { return HttpContext.GetOwinContext().GetUserManager(); } } private AppRoleManager RoleManager { get { return HttpContext.GetOwinContext().GetUserManager(); } } public ActionResult Index() { return View(RoleManager.Roles); } public ActionResult Create() { return View(); } public async Task Create(string name) { if (ModelState.IsValid) { IdentityResult result = await RoleManager.CreateAsync(new AppRole(name)); if (result.Succeeded) { return RedirectToAction("Index"); } else { AddErrorsFromResult(result); } } return View(name); } public async Task Delete(string id) { AppRole role = await RoleManager.FindByIdAsync(id); if (role != null) { IdentityResult result = await RoleManager.DeleteAsync(role); if (result.Succeeded) { return RedirectToAction("Index"); } else { return View("Error", result.Errors); } } else { return View("Error", new string { "Роль не найдена" }); } } private void AddErrorsFromResult(IdentityResult result) { foreach (string error in result.Errors) { ModelState.AddModelError("", error); } } } }

Здесь мы применили многие из тех приемов, что использовали в контроллере Admin, в том числе добавили свойства UserManager и RoleManager для более быстрого запроса объектов AppRoleManager и AppUserManager. Также мы добавили аналогичный метод AddErrorsFromResult(), который обрабатывает ошибки в объекте IdentityResult и добавляет их в метаданные модели.

Представления для контроллера RoleAdmin содержат простую HTML-разметку и операторы Razor. Нам необходимо отобразить не только список ролей, но и имена всех пользователей, входящих в каждую роль. Класс IdentityRole определяет свойство Users, которое возвращает коллекцию объектов IdentityUserRole , описывающих пользователей роли. Каждый объект IdentityUserRole имеет свойство UserId, которое возвращает уникальный идентификатор пользователя, с помощью которого мы будем получать имя пользователя.

Добавьте файл класса IdentityHelpers.cs в папку Infrastructure со следующим содержимым:

Using System.Web; using System.Web.Mvc; using Microsoft.AspNet.Identity.Owin; namespace Users.Infrastructure { public static class IdentityHelpers { public static MvcHtmlString GetUserName(this HtmlHelper html, string id) { AppUserManager mgr = HttpContext.Current .GetOwinContext().GetUserManager(); return new MvcHtmlString(mgr.FindByIdAsync(id).Result.UserName); } } }

Этот код содержит определение вспомогательного метода HTML , определенного как расширение класса HtmlHelper. Метод GetUserName() принимает строковый аргумент, содержащий идентификатор пользователя, получает экземпляр класса AppUserManager с помощью метода GetOwinContext().GetUserManager() (где метод GetOwinContext является расширяющим HttpContext), использует метод FindByIdAsync(), чтобы найти экземпляр AppUser, связанный с идентификатором и возвращает значение свойства UserName.

Следующий пример показывает содержимое файла Index.cshtml, находящегося в папке /Views/RoleAdmin:

@using Users.Models @using Users.Infrastructure @model IEnumerable @{ ViewBag.Title = "Роли"; }

Roles
@if (Model.Count() == 0) { } else { foreach (AppRole role in Model) { } }
ID Название Пользователи
Нет ролей
@role.Id @role.Name @if (role.Users == null || role.Users.Count == 0) { @: Нет пользователей в этой роли } else {

@string.Join(", ", role.Users.Select(x => Html.GetUserName(x.UserId)))

}
@using (Html.BeginForm("Delete", "RoleAdmin", new { id = role.Id })) { @Html.ActionLink("Изменить", "Edit", new { id = role.Id }, new { @class = "btn btn-primary btn-xs", style= "float:left; margin-right:5px" }) }
@Html.ActionLink("Создать", "Create", null, new { @class = "btn btn-primary" })

В этом представлении отображается список ролей, определенных в приложении, вместе со списком пользователей в каждой роли. На данный момент мы еще не создали ни одной роли:

Следующий пример содержит представление Create.cshtml в той же папке, которое используется для создания новых ролей:

@model string @{ ViewBag.Title = "Создание роли"; }

Создать роль

@Html.ValidationSummary(false) @using (Html.BeginForm()) {

Единственная информация, которая требуется для создания новой роли - ее название. Поэтому мы добавили один стандартный элемент и кнопку отправки формы POST-методу действия Create.

Чтобы протестировать функционал создания ролей, запустите приложение и перейдите по адресу /RoleAdmin/Index в окне браузера. Чтобы создать новую роль нажмите кнопку «Создать», введите имя в поле ввода в появившейся форме и нажмите вторую кнопку «Создать». Новое представление будет отображать список ролей, сохраненных в базе данных:

Вы можете также удалить роль из приложения нажав кнопку «Удалить».

Редактирование ролей

Для авторизации пользователей недостаточно просто создавать и удалять роли. Мы также должны уметь управлять ролями, назначать и удалять пользователей из роли. Это не сложный процесс, но для его реализации нам необходимо загружать данные о ролях с помощью класса AppRoleManager, а затем вызывать методы, определенные в классе AppUserManager на объектах, связанных с определенной ролью.

Давайте начнем с добавления новых классов модели-представления (view-model) в файл UserViewModels.cs:

Using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Users.Models { public class CreateModel { // ... } public class LoginViewModel { // ... } public class RoleEditModel { public AppRole Role { get; set; } public IEnumerable Members { get; set; } public IEnumerable NonMembers { get; set; } } public class RoleModificationModel { public string RoleName { get; set; } public string IdsToAdd { get; set; } public string IdsToDelete { get; set; } } }

Класс RoleEditModel содержит информацию о роли и определяет список пользователей в роли в виде коллекции объектов AppUser. Благодаря этому, мы сможем извлечь ID и имя каждого пользователя в роли. Класс RoleModificationModel будет получать данные от системы привязки модели во время редактирования данных пользователя. Он содержит массив идентификаторов пользователей, а не объектов AppUser, для замены ролей.

Определившись с классами моделей, давайте добавим методы редактирования ролей Edit в контроллер RoleAdmin:

Using System.Web; using System.Web.Mvc; using System.Threading.Tasks; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using System.ComponentModel.DataAnnotations; using Users.Infrastructure; using Users.Models; using System.Linq; using System.Collections.Generic; namespace Users.Controllers { public class RoleAdminController: Controller { // ... public async Task Edit(string id) { AppRole role = await RoleManager.FindByIdAsync(id); string memberIDs = role.Users.Select(x => x.UserId).ToArray(); IEnumerable members = UserManager.Users.Where(x => memberIDs.Any(y => y == x.Id)); IEnumerable nonMembers = UserManager.Users.Except(members); return View(new RoleEditModel { Role = role, Members = members, NonMembers = nonMembers }); } public async Task Edit(RoleModificationModel model) { IdentityResult result; if (ModelState.IsValid) { foreach (string userId in model.IdsToAdd ?? new string { }) { result = await UserManager.AddToRoleAsync(userId, model.RoleName); if (!result.Succeeded) { return View("Error", result.Errors); } } foreach (string userId in model.IdsToDelete ?? new string { }) { result = await UserManager.RemoveFromRoleAsync(userId, model.RoleName); if (!result.Succeeded) { return View("Error", result.Errors); } } return RedirectToAction("Index"); } return View("Error", new string { "Роль не найдена" }); } } }

Большая часть кода в GET-версии метода Edit отвечает за формирование списков пользователей входящих и не входящих в роль и реализуется с помощью методов LINQ. После группировки пользователей возвращается представление, которому передается объект RoleEditModel.

POST-версия метода Edit отвечает за добавление и удаление пользователей из ролей. Класс AppUserManager наследует ряд вспомогательных методов для работы с ролями из класса UserManager. Эти методы перечислены в таблице ниже:

Странность этих методов заключается в том, что они работают с идентификатором пользователя и именем роли, хотя каждая роль также имеет свой уникальный идентификатор. Именно поэтому класс RoleModificationModel содержит строковое свойство RoleName.

В примере ниже показан код представления Edit.cshtml, находящегося в папке /Views/RoleAdmin.cshtml:

@using Users.Models @model RoleEditModel @{ ViewBag.Title = "Изменить роль"; }

Изменить роль

@Html.ValidationSummary() @using (Html.BeginForm()) {
Добавить в роль @Model.Role.Name
@if (Model.NonMembers.Count() == 0) { } else { foreach (AppUser user in Model.NonMembers) { } }
Все пользователи в роли
User ID Добавить в роль
@user.UserName
Удалить из роли @Model.Role.Name
@if (Model.Members.Count() == 0) { } else { foreach (AppUser user in Model.Members) { } }
Нет пользователей в роли
User ID Удалить из роли
@user.UserName
@Html.ActionLink("Отмена", "Index", null, new { @class = "btn btn-default" }) }

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

Давайте протестируем функциональность редактирования ролей. Добавление класса AppRoleManager в архитектуру OWIN заставит Entity Framework удалить базу данных и воссоздать новую схему. Это означает, что пользователи, которых мы создали ранее исчезнут. Поэтому после запуска приложения перейдите по адресу /Admin/Index и создайте нескольких пользователей.

Чтобы проверить редактирование ролей, перейдите по адресу /RoleAdmin/Index и создайте несколько ролей, затем отредактируйте эти роли, добавив в них нескольких пользователей. На рисунке ниже показан пример приложения (я создал роль Users):

Использование ролей для авторизации

Теперь, когда у нас есть возможность управления ролями, мы можем использовать их в качестве основы для авторизации через атрибут Authorize. Чтобы проще было тестировать процесс авторизации, давайте добавим метод действия для выхода пользователя из системы в контроллер Account, как показано в примере ниже:

Public class AccountController: Controller { // ... public ActionResult Logout() { AuthManager.SignOut(); return RedirectToAction("Index", "Home"); } // ... }

Давайте обновим контроллер Home и добавим новый метод действия, который будет передавать информацию об аутентифицированном пользователе в представление:

Using System.Collections.Generic; using System.Web.Mvc; namespace Users.Controllers { public class HomeController: Controller { public ActionResult Index() { return View(GetData("Index")); } public ActionResult OtherAction() { return View("Index", GetData("OtherAction")); } private Dictionary GetData(string actionName) { Dictionary dict = new Dictionary(); dict.Add("Action", actionName); dict.Add("Пользователь", HttpContext.User.Identity.Name); dict.Add("Аутентифицирован?", HttpContext.User.Identity.IsAuthenticated); dict.Add("Тип аутентификации", HttpContext.User.Identity.AuthenticationType); dict.Add("В роли Users?", HttpContext.User.IsInRole("Users")); return dict; } } }

В этом примере мы оставили атрибут Authorize для метода действия Index без изменений, но добавили этот атрибут к методу OtherAction, задав при этом свойство Roles, ограничивающее доступ к этому методу только для пользователей, являющихся членами роли Users. Мы также добавили метод GetData(), который добавляет некоторую базовую информацию о пользователе, используя свойства, доступные через объект HttpContext.

В заключение, нам необходимо добавить кнопку выхода из приложения в представление Index.cshtml из папки /Views/Home:

... @Html.ActionLink("Выйти", "Logout", "Account", null, new {@class = "btn btn-primary"})

Атрибут Authorize может быть также использован для настройки авторизации на основе списка пользователей. Данную возможность удобно использовать в небольших проектах, но это создаст трудности при расширении приложения, т. к. каждый раз потребуется изменять код в контроллере, когда будет добавляется новый пользователь. Использование ролей для авторизации изолирует приложение от изменений в учетных записях отдельных пользователей и контролирует доступ к приложению через членство ролей.

Для тестирования системы авторизации, запустите приложение и перейдите по адресу /Home/Index. Браузер будет перенаправлен на страницу входа в приложение, где вы должны будете ввести данные существующей учетной записи. Метод действия Index является доступным для любого авторизованного пользователя. Однако если вы перейдете по адресу /Index/OtherAction, доступ будет открыт только тем пользователям, которые являются членами роли Users.

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

Public class AccountController: Controller { public ActionResult Login(string returnUrl) { if (HttpContext.User.Identity.IsAuthenticated) { return View("Error", new string { "В доступе отказано" }); } ViewBag.returnUrl = returnUrl; return View(); } // ... }

На рисунке ниже наглядно показано поведение нашего приложения, когда пользователю отказано в доступе:

Поисковая система Google (www.google.com) предоставляет множество возможностей для поиска. Все эти возможности – неоценимый инструмент поиска для пользователя впервые попавшего в Интернет и в то же время еще более мощное оружие вторжения и разрушения в руках людей с злыми намерениями, включая не только хакеров, но и некомпьютерных преступников и даже террористов.
(9475 просмотров за 1 неделю)

Денис Батранков
denisNOSPAMixi.ru

Внимание: Эта статья не руководство к действию. Эта статья написана для Вас, администраторы WEB серверов, чтобы у Вас пропало ложное ощущение, что Вы в безопасности, и Вы, наконец, поняли коварность этого метода получения информации и взялись за защиту своего сайта.

Введение

Я, например, за 0.14 секунд нашел 1670 страниц!

2. Введем другую строку, например:

inurl:"auth_user_file.txt"

немного меньше, но этого уже достаточно для свободного скачивания и для подбора паролей (при помощи того же John The Ripper). Ниже я приведу еще ряд примеров.

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

Впервые информация о Google Hacking появилась на рассылке Bugtruck еще 3 года назад. В 2001 году эта тема была поднята одним французским студентом. Вот ссылка на это письмо http://www.cotse.com/mailing-lists/bugtraq/2001/Nov/0129.html . В нем приведены первые примеры таких запросов:

1) Index of /admin
2) Index of /password
3) Index of /mail
4) Index of / +banques +filetype:xls (for france...)
5) Index of / +passwd
6) Index of / password.txt

Нашумела эта тема в англо-читающей части Интернета совершенно недавно: после статьи Johnny Long вышедшей 7 мая 2004 года. Для более полного изучения Google Hacking советую зайти на сайт этого автора http://johnny.ihackstuff.com . В этой статье я лишь хочу ввести вас в курс дела.

Кем это может быть использовано:
- Журналисты, шпионы и все те люди, кто любит совать нос не в свои дела, могут использовать это для поиска компромата.
- Хакеры, разыскивающие подходящие цели для взлома.

Как работает Google.

Для продолжения разговора напомню некоторые из ключевых слов, используемых в запросах Google.

Поиск при помощи знака +

Google исключает из поиска неважные, по его мнению, слова. Например вопросительные слова, предлоги и артикли в английском языке: например are, of, where. В русском языке Google, похоже, все слова считает важными. Если слово исключается из поиска, то Google пишет об этом. Чтобы Google начал искать страницы с этими словами перед ними нужно добавить знак + без пробела перед словом. Например:

ace +of base

Поиск при помощи знака –

Если Google находит большое количество станиц, из которых необходимо исключить страницы с определенной тематикой, то можно заставить Google искать только страницы, на которых нет определенных слов. Для этого надо указать эти слова, поставив перед каждым знак – без пробела перед словом. Например:

рыбалка -водка

Поиск при помощи знака ~

Возможно, что вы захотите найти не только указанное слово, но также и его синонимы. Для этого перед словом укажите символ ~.

Поиск точной фразы при помощи двойных кавычек

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

"подставка для книг"

Чтобы было хоть одно из указанных слов нужно указать логическую операцию явно: OR. Например:

книга безопасность OR защита

Кроме того в строке поиска можно использовать знак * для обозначения любого слова и. для обозначения любого символа.

Поиск слов при помощи дополнительных операторов

Существуют поисковые операторы, которые указываются в строке поиска в формате:

operator:search_term

Пробелы рядом с двоеточием не нужны. Если вы вставите пробел после двоеточия, то увидите сообщение об ошибке, а перед ним, то Google будет использовать их как обычную строку для поиска.
Существуют группы дополнительных операторов поиска: языки - указывают на каком языке вы хотите увидеть результат, дата - ограничивают результаты за прошедшие три, шесть или 12 месяцев, вхождения - указывают в каком месте документа нужно искать строку: везде, в заголовке, в URL, домены - производить поиск по указанному сайту или наоборот исключить его из поиска, безопасный поиск - блокируют сайты содержащие указанный тип информации и удаляют их со страниц результатов поиска.
При этом некоторые операторы не нуждаются в дополнительном параметре, например запрос "cache:www.google.com " может быть вызван, как полноценная строка для поиска, а некоторые ключевые слова, наоборот, требуют наличия слова для поиска, например " site:www.google.com help ". В свете нашей тематики посмотрим на следующие операторы:

Оператор

Описание

Требует дополнительного параметра?

поиск только по указанному в search_term сайту

поиск только в документах с типом search_term

найти страницы, содержащие search_term в заголовке

найти страницы, содержащие все слова search_term в заголовке

найти страницы, содержащие слово search_term в своем адресе

найти страницы, содержащие все слова search_term в своем адресе

Оператор site: ограничивает поиск только по указанному сайту, причем можно указать не только доменное имя, но и IP адрес. Например, введите:

Оператор filetype: ограничивает поиск в файлах определенного типа. Например:

На дату выхода статьи Googlе может искать внутри 13 различных форматов файлов:

  • Adobe Portable Document Format (pdf)
  • Adobe PostScript (ps)
  • Lotus 1-2-3 (wk1, wk2, wk3, wk4, wk5, wki, wks, wku)
  • Lotus WordPro (lwp)
  • MacWrite (mw)
  • Microsoft Excel (xls)
  • Microsoft PowerPoint (ppt)
  • Microsoft Word (doc)
  • Microsoft Works (wks, wps, wdb)
  • Microsoft Write (wri)
  • Rich Text Format (rtf)
  • Shockwave Flash (swf)
  • Text (ans, txt)

Оператор link: показывает все страницы, которые указывают на указанную страницу.
Наверно всегда интересно посмотреть, как много мест в Интернете знают о тебе. Пробуем:

Оператор cache: показывает версию сайта в кеше Google, как она выглядела, когда Google последний раз посещал эту страницу. Берем любой, часто меняющийся сайт и смотрим:

Оператор intitle: ищет указанное слово в заголовке страницы. Оператор allintitle: является расширением – он ищет все указанные несколько слов в заголовке страницы. Сравните:

intitle:полет на марс
intitle:полет intitle:на intitle:марс
allintitle:полет на марс

Оператор inurl: заставляет Google показать все страницы содержащие в URL указанную строку. Оператор allinurl: ищет все слова в URL. Например:

allinurl:acid acid_stat_alerts.php

Эта команда особенно полезна для тех, у кого нет SNORT – хоть смогут посмотреть, как он работает на реальной системе.

Методы взлома при помощи Google

Итак, мы выяснили что, используя комбинацию вышеперечисленных операторов и ключевых слов, любой человек может заняться сбором нужной информации и поиском уязвимостей. Эти технические приемы часто называют Google Hacking.

Карта сайта

Можно использовать оператор site: для просмотра всех ссылок, которые Google нашел на сайте. Обычно страницы, которые динамически создаются скриптами, при помощи параметров не индексируются, поэтому некоторые сайты используют ISAPI фильтры, чтобы ссылки были не в виде /article.asp?num=10&dst=5 , а со слешами /article/abc/num/10/dst/5 . Это сделано для того, чтобы сайт вообще индексировался поисковиками.

Попробуем:

site:www.whitehouse.gov whitehouse

Google думает, что каждая страница сайта содержит слово whitehouse. Этим мы и пользуемся, чтобы получить все страницы.
Есть и упрощенный вариант:

site:whitehouse.gov

И что самое приятное - товарищи с whitehouse.gov даже не узнали, что мы посмотрели на структуру их сайта и даже заглянули в кэшированные странички, которые скачал себе Google. Это может быть использовано для изучения структуры сайтов и просмотра содержимого, оставаясь незамеченным до поры до времени.

Просмотр списка файлов в директориях

WEB серверы могут показывать списки директорий сервера вместо обычных HTML страниц. Обычно это делается для того, чтобы пользователи выбирали и скачивали определенные файлы. Однако во многих случаях у администраторов нет цели показать содержимое директории. Это возникает вследствие неправильной конфигурации сервера или отсутствия главной страницы в директории. В результате у хакера появляется шанс найти что-нибудь интересное в директории и воспользоваться этим для своих целей. Чтобы найти все такие страницы, достаточно заметить, что все они содержат в своем заголовке слова: index of. Но поскольку слова index of содержат не только такие страницы, то нужно уточнить запрос и учесть ключевые слова на самой странице, поэтому нам подойдут запросы вида:

intitle:index.of parent directory
intitle:index.of name size

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

Получение версии WEB сервера.

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

Apache1.3.29 - ProXad Server at trf296.free.fr Port 80

Опытный администратор может подменить эту информацию, но, как правило, она соответствует истине. Таким образом, чтобы получить эту информацию достаточно послать запрос:

intitle:index.of server.at

Чтобы получить информацию для конкретного сервера уточняем запрос:

intitle:index.of server.at site:ibm.com

Или наоборот ищем сервера работающие на определенной версии сервера:

intitle:index.of Apache/2.0.40 Server at

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

Также можно получить версию сервера, просматривая страницы, которые по умолчанию устанавливаются при установке свежей версии WEB сервера. Например, чтобы увидеть тестовую страницу Apache 1.2.6 достаточно набрать

intitle:Test.Page.for.Apache it.worked!

Мало того, некоторые операционные системы при установке сразу ставят и запускают WEB сервер. При этом некоторые пользователи даже об этом не подозревают. Естественно если вы увидите, что кто-то не удалил страницу по умолчанию, то логично предположить, что компьютер вообще не подвергался какой-либо настройке и, вероятно, уязвим для атак.

Попробуйте найти страницы IIS 5.0

allintitle:Welcome to Windows 2000 Internet Services

В случае с IIS можно определить не только версию сервера, но и версию Windows и Service Pack.

Еще одним способом определения версии WEB сервера является поиск руководств (страниц подсказок) и примеров, которые могут быть установлены на сайте по умолчанию. Хакеры нашли достаточно много способов использовать эти компоненты, чтобы получить привилегированный доступ к сайту. Именно поэтому нужно на боевом сайте удалить эти компоненты. Не говоря уже о том, что по наличию этих компонентов можно получить информацию о типе сервера и его версии. Например, найдем руководство по apache:

inurl:manual apache directives modules

Использование Google как CGI сканера.

CGI сканер или WEB сканер – утилита для поиска уязвимых скриптов и программ на сервере жертвы. Эти утилиты должны знать что искать, для этого у них есть целый список уязвимых файлов, например:

/cgi-bin/cgiemail/uargg.txt
/random_banner/index.cgi
/random_banner/index.cgi
/cgi-bin/mailview.cgi
/cgi-bin/maillist.cgi
/cgi-bin/userreg.cgi

/iissamples/ISSamples/SQLQHit.asp
/SiteServer/admin/findvserver.asp
/scripts/cphost.dll
/cgi-bin/finger.cgi

Мы может найти каждый из этих файлов с помощью Google, используя дополнительно с именем файла в строке поиска слова index of или inurl: мы можем найти сайты с уязвимыми скриптами, например:

allinurl:/random_banner/index.cgi

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

Как защитить себя от взлома через Google.

1. Не выкладывайте важные данные на WEB сервер.

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

2. Проверьте свой сайт.

Используйте описанные методы, для исследования своего сайта. Проверяйте периодически свой сайт новыми методами, которые появляются на сайте http://johnny.ihackstuff.com . Помните, что если вы хотите автоматизировать свои действия, то нужно получить специальное разрешение от Google. Если внимательно прочитать http://www.google.com/terms_of_service.html , то вы увидите фразу: You may not send automated queries of any sort to Google"s system without express permission in advance from Google.

3. Возможно, вам не нужно чтобы Google индексировал ваш сайт или его часть.

Google позволяет удалить ссылку на свой сайт или его часть из своей базы, а также удалить страницы из кэша. Кроме того вы можете запретить поиск изображений на вашем сайте, запретить показывать короткие фрагменты страниц в результатах поиска Все возможности по удалению сайта описаны на сранице http://www.google.com/remove.html . Для этого вы должны подтвердить, что вы действительно владелец этого сайта или вставить на страницу теги или

4. Используйте robots.txt

Известно, что поисковые машины заглядывают в файл robots.txt лежащий в корне сайта и не индексируют те части, которые помечены словом Disallow . Вы можете воспользоваться этим, для того чтобы часть сайта не индексировалась. Например, чтобы не индексировался весь сайт, создайте файл robots.txt содержащий две строчки:

User-agent: *
Disallow: /

Что еще бывает

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

Приложение.

Немного сладкого. Попробуйте сами что-нибудь из следующего списка:

1. #mysql dump filetype:sql - поиск дампов баз данных mySQL
2. Host Vulnerability Summary Report - покажет вам какие уязвимости нашли другие люди
3. phpMyAdmin running on inurl:main.php - это заставит закрыть управление через панель phpmyadmin
4. not for distribution confidential
5. Request Details Control Tree Server Variables
6. Running in Child mode
7. This report was generated by WebLog
8. intitle:index.of cgiirc.config
9. filetype:conf inurl:firewall -intitle:cvs – может кому нужны кофигурационные файлы файрволов? :)
10. intitle:index.of finances.xls – мда....
11. intitle:Index of dbconvert.exe chats – логи icq чата
12. intext:Tobias Oetiker traffic analysis
13. intitle:Usage Statistics for Generated by Webalizer
14. intitle:statistics of advanced web statistics
15. intitle:index.of ws_ftp.ini – конфиг ws ftp
16. inurl:ipsec.secrets holds shared secrets – секретный ключ – хорошая находка
17. inurl:main.php Welcome to phpMyAdmin
18. inurl:server-info Apache Server Information
19. site:edu admin grades
20. ORA-00921: unexpected end of SQL command – получаем пути
21. intitle:index.of trillian.ini
22. intitle:Index of pwd.db
23. intitle:index.of people.lst
24. intitle:index.of master.passwd
25. inurl:passlist.txt
26. intitle:Index of .mysql_history
27. intitle:index of intext:globals.inc
28. intitle:index.of administrators.pwd
29. intitle:Index.of etc shadow
30. intitle:index.of secring.pgp
31. inurl:config.php dbuname dbpass
32. inurl:perform filetype:ini

  • "Hacking mit Google"
  • Учебный центр "Информзащита" http://www.itsecurity.ru - ведущий специализированный центр в области обучения информационной безопасности (Лицензия Московского Комитета образования № 015470, Государственная аккредитация № 004251). Единственный авторизованный учебный центр компаний Internet Security Systems и Clearswift на территории России и стран СНГ. Авторизованный учебный центр компании Microsoft (специализация Security). Программы обучения согласованы с Гостехкомиссией России, ФСБ (ФАПСИ). Свидетельства об обучении и государственные документы о повышении квалификации.

    Компания SoftKey – это уникальный сервис для покупателей, разработчиков, дилеров и аффилиат–партнеров. Кроме того, это один из лучших Интернет-магазинов ПО в России, Украине, Казахстане, который предлагает покупателям широкий ассортимент, множество способов оплаты, оперативную (часто мгновенную) обработку заказа, отслеживание процесса выполнения заказа в персональном разделе, различные скидки от магазина и производителей ПО.



    Есть вопросы?

    Сообщить об опечатке

    Текст, который будет отправлен нашим редакторам: