Nicholas K. Dionysopoulos

Профессиональный PHP-разработчик. Руководитель Akeeba Ltd и ведущий разработчик Akeeba Backup для WordPress, Joomla! и standalone.

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

Вы можете задаться вопросом, сознательно ли разработчики публикуют неработающий код? Отнюдь нет, они это проверили... но они сделали это только в очень узком случае использования, в котором они ожидают, что их плагины будут использоваться. Это называется “тестированием счастливого пути” ("happy path testing") и почти так же плохо, как отсутствие тестирования вообще. Проблема заключается в том, что когда плагин используется в любом другом контексте — приложение CLI, вывод не в формате HTML, в случаях, когда формат вывода может быть определен только после завершения выполнения компонента для страницы, - они приведут к непреднамеренным оказиям, т.е. сайт сломается.

Что еще хуже, клиенты начинают обвинять только невиновные стороны - саму Joomla и сторонних разработчиков, чье программное обеспечение написано правильно и работает отлично. Я должен сказать, что мы получаем по крайней мере два тикета каждую неделю в Akeeba Ltd по поводу такого рода проблемных плагинов.

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

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

Симфония ошибок

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

class plgSystemFoobar extends \Joomla\CMS\Plugin\CMSPlugin
{
	public function __construct(&$subject, $config)
	{
		parent::__construct($subject, $config);

		if (\Joomla\CMS\Factory::getApplication()->isClient('administrator')) return;

		$document = \Joomla\CMS\Factory::getDocument();
		$document->addScript(\Joomla\CMS\Uri\Uri::root(true) . 'plugins/system/foobar/js/foobar.js');
	}
}

Этот код обманчиво выглядит как простой системный плагин. Он добавляет файл JavaScript к каждой загружаемой странице во внешнем интерфейсе. Верно?

Что ж, это то, что он хочет делать, но не то, что он делает на самом деле. Он также нарушает CLI и API-приложения Joomla 4, ломает страницы с выводом, отличным от HTML, запрещает компонентам использовать вывод, отличный от HTML, и пытается загрузить файл JavaScript из неправильного места. Четыре ошибки в пяти строках кода.

Не выполняйте логику в конструкторе класса плагина

Давайте подумаем о цикле Joomla. В общих чертах, запрос в конечном итоге обрабатывается в Joomla файлом index.php. Он заводит Joomla! приложение, например \Joomla\CMS\Application\SiteApplication для фронтенда. Основной точкой входа для объекта приложения является метод doExecute. Этот метод выполняет большую часть инициализации перед маршрутизацией и диспетчеризацией приложения, что означает, что эта инициализация выполняется до того, как Joomla проанализирует SEF URL-адреса или создаст объект документа. Фактически, Joomla загрузит все включенные системные плагины до того, как выяснит что либо о самой себе.

Разработчик этого плагина поместил свою бизнес-логику в конструктор плагина, который выполняется на этой ранней стадии инициализации приложения Joomla. В то время как isClient() будет работать, остальная часть кода, которая пытается получить объект документа, является неправильным кодом, который нарушает работу сайта.

Плагин ошибочно использует метод \Joomla\CMS\Factory::getDocument(), чтобы получить документ. Это устарело в Joomla 4 и будет удалено в Joomla 5. Предполагается, что вы будете использовать метод getDocument() объекта приложения. Если бы разработчик сделал это, он бы получил значение null, потому что документ еще не создан.

Практически, весь этот код должен быть перенесен из конструктора объекта плагина в метод onAfterDispatch, чтобы работать правильно.

Не используйте \Joomla\CMS\Factory

Вторая проблема этого плагина - ещё одна веская причина, по которой метод Factory::getDocument() устарел.

Вызов Factory::getDocument() принудительно создаст объект документа, который затем будет использоваться объектом приложения. Объект документа создается на основе информации, содержащейся в запросе. Однако, как вы, возможно, помните, в этот момент выполнения приложения Joomla еще не проанализировала SEF url! Тот же эффект был бы достигнут, если бы этот код был вызывался при событии onAfterInitialise - самом раннем событии для системных плагинов, инициируемом Joomla.

Поскольку SEF URL-адрес не был проанализирован, Joomla не может достоверно знать тип используемого документа. Подумайте, например, о таком URL-адресе, как https://www.example.com/foobar.json который при прохождении через маршрутизатор URL-адреса SEF, помимо прочего, установит format=json в запросе. Это означает, что этот запрос ожидает, что Joomla создаст объект документа \Joomla\CMS\Document\JsonDocument.

Однако, поскольку format=json еще не задан, Joomla примет format=html при вызове getDocument(), поэтому будет создан объект документа \Joomla\CMS\Document\HTMLDocument, который также будет использоваться приложением. Это, конечно, приведет к поломке компонента, который обрабатывает запрос, причем вина лежит исключительно на авторе косячного плагина.

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

Вы должны использовать ТОЛЬКО ДВА метода \Joomla\CMS\Factory в Joomla 4:

  • getContainer() - возвращает контейнер внедрения зависимостей Joomla (DI Container, иногда сокращенно DIC).

  • getApplication() - возвращает текущий объект приложения Joomla, обрабатывающий запрос.

Всё. Больше ничего другого использовать не нужно! Всё остальное предоставляется либо через DI-контейнер, либо через сам объект приложения.

Чтобы получить документ приложения используйте \Joomla\CMS\Factory::getApplication()->getDocument().

В Joomla есть вывод не только HTML

Это до неприличия распространённая ошибка. Разработчики, похоже, предполагают, что Joomla всегда будет генерировать только HTML-вывод. ТАКОГО НЕ БЫЛО С ТЕХ ПОР, КАК JOOMLA 1.0 БЫЛА ВЫПУЩЕНА В 2005 ГОДУ, ЗАПОМНИТЕ ЭТО, ХРИСТА РАДИ! Серьезно, люди! Это касалось даже Mambo, предшественницы Joomla. Joomla - это не WordPress, она вполне способна генерировать выходные данные, отличные от HTML, такие как XML, JSON, RSS-каналы, Atom-каналы, необработанные raw binary (например, изображения) и так далее.

Разработчик этого плагина сделал необоснованное предположение, что этот $document всегда будет содержать HTMLDocument.

URL-адрес, подобный https://www.example.com/index.php?option=com_whatever&format=json — несмотря на две упомянутые выше проблемы — все равно будет заполнять $document объектом JSONDocument. Однако в JSONDocument нет метода addScript. Поэтому этот плагин сразу же вызывает фатальную ошибку PHP.

Правильным подходом будет сделать “feature detection”:

$document = \Joomla\CMS\Factory::getApplication()->getDocument();

if (!($document instanceof \Joomla\CMS\Document\HtmlDocument))
{
	return;
}

Если документ, который возвращает наше приложение, не является HTMLDocument - валим отсюда. Просто, не правда ли?

В Joomla есть не только frontend и backend

Теперь давайте перейдем к самой большой ошибке из всех: предположим, что Joomla полностью состоит из интерфейса (сайта) и серверного приложения (администратора). Этого не было со времен Joomla 1.6, выпущенной в 2010 году — на момент написания этой статьи прошло двенадцать лет, и я все еще вижу эту ошибку!

Эта строка неверна:

if (\Joomla\CMS\Factory::getApplication()->isClient('administrator')) return;

Очевидно, разработчик хотел сказать: “Если это не фронт сайта, ничего не делаем”. Вместо этого они на самом деле сказал: “Если это админка сайта — следовательно, это фронт сайта или API-приложения, или CLI-приложения, или любого пользовательского приложения, расширяющего класс WebApplication Joomla — ничего не делайте”. Упс...

Видите ли, Joomla 4 имеет несколько типов приложения из коробки:

  • installation - это веб-установщик при создании нового сайта. Сторонний код, такой как наш системный плагин symphony-of-mistakes, в нем не загружается и не касается сторонних разработчиков. Он существует со времен Joomla 1.0.

  • site - фронтенд сайта. Существует со времен Joomla 1.0

  • administrator - бэкенд сайта. Существует со времен Joomla 1.0

  • api - директория /api в корне сайта на Joomla 4. Появилось в Joomla 4.

  • cli -  cli/joomla.php CLI-приложение. Появилось в Joomla 4.

Кроме того, начиная с Joomla 1.5, существуют приложения, отличные от site и administrator.

Начиная с версии Joomla 1.5, появилась возможность создавать собственное пользовательское приложение, расширив JApplicationWeb. Эти пользовательские приложения загружают системные плагины по умолчанию. Они использовались для создания пользовательских точек входа для обратных вызовов, например, в платежных плагинах для компонентов электронной коммерции. Сейчас их использование не имеет смысла, с момента появления com_ajax в Joomla 2.5.

Начиная с Joomla 1.6 и вплоть до выхода Joomla 5.0 разработчик может создавать собственные приложения CLI, расширяя JApplicationCli. Эти приложения не загружают системные плагины по умолчанию, поэтому они вряд ли сломались.

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

Правильный способ сделать это - это, конечно, явно проверить тип приложения, с которым вы работаете:

if (!\Joomla\CMS\Factory::getApplication()->isClient('site')) return;

Не загружайте статические ресурсы напрямую и/или из папки вашего плагина

Это не ошибка, которая приведёт к поломке сайтов сегодня. Но она приведет к поломке сайтов с Joomla 5.0 и небольшой проблеме безопасности ????

Разработчик расширения решил загрузить свой статический файл JavaScript, используя устаревший метод addScript() документа, и разместил файл в файловой структуре плагина. Это проблема "два в одном".

Прежде всего нужно помнить, что с тех пор, как Joomla 1.5 (выпущена в 2007 году — 15 лет назад на момент написания этой статьи), в Joomla появилась папка media, в которой расширения должны размещать все общедоступные статические файлы, будь то статические файлы или пользовательский контент, управляемый вне медиа-менеджера Joomla.

Разработчик расширения должен был поместить свой файл JavaScript в папку media/plg_system_foobar/js, используя следующий раздел в XML-манифесте своего плагина:

<media folder="media" destination="plg_system_foobar">
    <folder>js</folder>
    <file>joomla.asset.json</file>
</media>

(Мы сейчас увидим, что представляет собой файл joomla.asset.json).

Это плохая практика с точки зрения безопасности сайта, когда смешивается исполняемый серверный код (файлы .php) с исполняемым кодом интерфейса (файлы .js, .es6 и т.д.) и статическими медиафайлами (CSS, изображения, видео и т.д.). Joomla движется к размещению всего доступного для фронтенда материала в папке media — даже для шаблонов, начиная с версии Joomla 4.1 — и, скорее всего, начнет применять средства контроля безопасности для предотвращения веб-доступа к папкам плагинов, компонентов, модулей и т.д.

Будем считать, что теперь Вы предупреждены.

Следующая проблема заключается в том, что метод addScript() в HTMLDocument устарел и отмечен как deprecated. Joomla 4 перешла на использование зависимостей веб-ассетов и Web Asset Manager для управления зависимостями ассетов!

От переводчика: в предыдущих статьях упоминал замечательную статью Как правильно подключать JavaScript и CSS в Joomla 4, а также дополнение к ней - статья на хабре Использование WebAssetsManager Joomla 4 и добавление собственных пресетов с помощью плагина - Т.С.

Веб-ассеты и их зависимости объявлены в файле joomla.asset.json, расположенном в подпапке расширения в каталоге media. Итак, разработчик должен был создать файл media/plg_system_foobar/joomla.asset.json со следующим содержимым:

{
	"$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
	"name": "plg_system_foobar",
	"version": "1.0.0",
	"description": "Foobar plugin",
	"license": "GPL-3.0-or-later",
	"assets": [
		{
			"name": "plg_system_foobar.foobar",
			"description": "Foobar JavaScript",
			"type": "script",
			"uri": "plg_system_foobar/foobar.js",
			"dependencies": [
				"core"
			],
			"attributes": {
				"defer": true
			}
		}
	]
}

Это позволяет разработчику указать Joomla использовать свой скрипт с помощью простой строчки кода:

$document->getWebAssetManager()->useScript('plg_system_foobar.foobar');

В чем загвоздка. Это безопасно, даже если вы не проверяете тип объекта документа. Да, если $document - это JSONDocument, в котором нет понятия веб-ресурсов, этот код все равно будет работать. Но, по-прежнему рекомендуется проверять тип документа так, как я вам сказал, чтобы избежать бессмысленной работы или появления ошибок в будущем.

Собираем всё воедино

Давайте соберем все, что мы узнали, воедино. К нашему плагину из пяти строк добавилась всего лишь ещё пара, и он больше не нарушает работу сайтов, на которых он установлен. Изменения выделены жирным шрифтом:

<?php
class plgSystemFoobar extends \Joomla\CMS\Plugin\CMSPlugin
{
  // 1-е изменение - onAfterDispatch
   public function onAfterDispatch()
   {
     // 2-е изменение - изменение условия на правильное
      if (!\Joomla\CMS\Factory::getApplication()->isClient('site')) return;
     // 3-е изменение - получаем Document из приложения
      $document = \Joomla\CMS\Factory::getApplication()->getDocument();
     // 4-е изменение - проверяем тип документа
      if (!($document instanceof \Joomla\CMS\Document\HtmlDocument))
      {
         return;
      }
     // Добавляем js-скрипт с помощью Web Assets Manager
      $document->getWebAssetManager()->useScript('plg_system_foobar.foobar');
   }
}

Этот код всё ещё короток. Этот код всё ещё читаем. Этот код (по большей части) перспективен, хотя он по-прежнему и использует устаревшую структуру CMSPlugin, но это тема для другой статьи.

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

Так же полезные ресурсы

Ресурсы сообщества:

Telegram:

Комментарии (5)


  1. Tomasina
    16.07.2022 22:27
    +3

    Джумла еще жива?


    1. sergeytolkachyov Автор
      17.07.2022 05:25
      +1

      Конечно, как и все остальные. С любопытными кейсами, между прочим https://vc.ru/life/428975-istoriya-vzleta-i-padeniya-novostnogo-sayta-ot-10-tysyach-do-1-mln-unikov-v-sutki-i-obratno-i-pri-chem-tut-joomla


    1. greenkey
      18.07.2022 02:09
      +2

      Joomla, как ни странно, растет в правильном направлении ;-) страдает же, она, на 99,9% от слабых разработчиков сторонних компонентов, с их дырами в безопасности.


  1. evgeniy_p
    18.07.2022 15:04

    Первая самая фатальная ошибка - начать писать на Joomla в 2к22 году.


    1. sergeytolkachyov Автор
      18.07.2022 15:07

      Почему же?