/
Plugins_Building.xml
711 lines (602 loc) · 24.7 KB
/
Plugins_Building.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
<?xml version='1.0' encoding='utf-8' ?>
<!DOCTYPE section PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [
<!ENTITY % BOOK_ENTITIES SYSTEM "Developers_Guide.ent">
%BOOK_ENTITIES;
]>
<section id="dev.plugins.building">
<title>Building a Plugin</title>
<para>
This section will act as a walk through of how to build a plugin, from the
bare basics all the way up to advanced topics. A general understanding of
the concepts covered in the last section is assumed, as well as knowledge
of how the event system works. Later topics in this section will require
knowledge of database schemas and how they are used with MantisBT.
</para>
<para>
This walk through will be working towards building a single end result: the
"Example" plugin as listed in <xref linkend="dev.plugins.building.source" />.
You may refer to the final source code along the way,
although every part of it will be built up in steps throughout this section.
</para>
<section id="dev.plugins.building.structure">
<title>Plugin Structure</title>
<para>
This section will introduce the general concepts of plugin structure,
and how to get a barebones plugin working with MantisBT. Not much will be
mentioned yet on the topic of adding functionality to plugins, just how to
get the development process rolling.
</para>
<para>
The backbone of every plugin is what MantisBT calls the
<emphasis>basename</emphasis>, a succinct, and most
importantly, unique name that identifies the plugin. It may
not contain any spacing or special characters beyond the
ASCII upper- and lowercase alphabet, numerals, and
underscore. This is used to identify the plugin everywhere
except for what the end-user sees. For our "Example" plugin,
the basename we will use should be obvious enough:
<literal>Example</literal>.
</para>
<para>
Every plugin must be contained in a single directory, named
to match the plugin's basename, as well as contain at least
a single PHP file, also named to match the basename, as such:
</para>
<para>
Note that for plugins that require a database schema to operate,
the basename is also used to build the table names, using the
MantisBT table prefixes and suffix (please refer to the
Admin Guide's <emphasis>Configuration</emphasis> section for
further information).
If our Example plugin were to create a table named 'foo',
assuming default values for prefixes and suffix in MantisBT
configuration, the physical table name would be
<literal>mantis_plugin_Example_foo_table</literal>.
</para>
<programlisting>
Example/
Example.php
</programlisting>
<warning>
<para>
Depending on case sensitivity of the underlying file system,
these names must <emphasis>exactly match</emphasis> the plugin's
base name, i.e. <literal>example</literal> will not work.
</para>
</warning>
<para>
This top-level PHP file must then contain a concrete class deriving from
the <classname>MantisPlugin</classname> class, which must be named in the
form of <classname>%Basename%Plugin</classname>, which for our purpose
becomes <classname>ExamplePlugin</classname>.
</para>
<para>
Because of how <classname>MantisPlugin</classname> declares the
<function>register()</function> method as <literal>abstract</literal>, our
plugin must implement that method before PHP will find it semantically
valid. This method is meant for one simple purpose, and should never be
used for any other task: setting the plugin's information properties
including the plugin's name, description, version, and more.
Please refer to <xref linkend="dev.plugins.building.properties" />
below for details about available properties.
</para>
<para>
Once your plugin defines its class, implements the <function>register()</function>
method, and sets at least the name and version properties, it is then
considered a "complete" plugin, and can be loaded and installed within
MantisBT's plugin manager. At this stage, our Example plugin, with all the
possible plugin properties set at registration, looks like this:
</para>
<programlisting><filename>Example/Example.php</filename>
<?php
class ExamplePlugin extends MantisPlugin {
function register() {
$this->name = 'Example'; # Proper name of plugin
$this->description = ''; # Short description of the plugin
$this->page = ''; # Default plugin page
$this->version = '1.0'; # Plugin version string
$this->requires = array( # Plugin dependencies
'MantisCore' => '2.0', # Should always depend on an appropriate
# version of MantisBT
);
$this->author = ''; # Author/team name
$this->contact = ''; # Author/team e-mail address
$this->url = ''; # Support webpage
}
}
</programlisting>
<para>
This alone will allow the Example plugin to be installed with MantisBT, and
is the foundation of any plugin. More of the plugin development process
will be continued in the next sections.
</para>
</section>
<section id="dev.plugins.building.properties">
<title>Properties</title>
<para>
This section describes the properties that can be defined when
registering the plugin.
</para>
<variablelist>
<varlistentry>
<term>name</term>
<listitem>
<para>Your plugin's full name.
<emphasis>Required value.</emphasis>
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>description</term>
<listitem>
<para>A full description of your plugin.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>page</term>
<listitem>
<para>The name of a plugin page for further information
and administration of the plugin.
This is used to create a link to the specified page
on Mantis' manage plugin page.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>version</term>
<listitem>
<para>Your plugin's version string.
<emphasis>Required value.</emphasis>
We recommend following the
<ulink url="https://semver.org/">Semantic Versioning</ulink>
specification, but you are free to use any
versioning scheme that can be handled by PHP's
<ulink url="https://www.php.net/manual/en/function.version-compare.php">
version_compare()
</ulink> function.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>requires</term>
<listitem>
<para>An array of key/value pairs of basename/version
plugin dependencies.
<note>
<para>The special, reserved basename
<literal>MantisCore</literal> can be used to
specify the minimum requirement for MantisBT
core.
</para>
</note>
The version string can be defined as:
</para>
<itemizedlist>
<listitem>
<para><emphasis>Minimum requirement</emphasis>:
the plugin specified by the given basename
must be installed, and its version must be
equal or higher than the indicated one.
</para>
</listitem>
<listitem>
<para><emphasis>Maximum requirement</emphasis>:
prefixing a version number with
'<literal><</literal>' will allow the
plugin to specify the highest version
(non-inclusive) up to which the required
dependency is supported.
<note>
<para>If the plugin's minimum dependency
for MantisCore is unspecified or lower
than the current release (i.e. it does
not specifically list the current core
version as supported) and the plugin
does not define a maximum dependency,
a default one will be set to the next
major release of MantisBT.
(i.e. for 2.x.y we would add
<literal>'<2</literal>').
</para>
<para>This effectively disables plugins
which have not been specifically designed
for a new major Mantis release, thus
forcing authors to review their code,
adapt it if necessary, and release a
new version of the plugin with updated
dependencies.
</para>
</note>
</para>
</listitem>
<listitem>
<para><emphasis>Both minimum and maximum</emphasis>:
the two version numbers must be separated by
a comma.
</para>
</listitem>
</itemizedlist>
<para>
Here are a few examples to illustrate the above
explanations, assuming that the current Mantis
release (<emphasis>MantisCore</emphasis> version)
is 2.1:
<itemizedlist>
<listitem>
<para>Old release without a maximum version
specified
<programlisting>
$this->requires = array( 'MantisCore' => '1.3.1' );
</programlisting>
The plugin is compatible with MantisBT
>= 1.3.1 and < 2.0.0 - note that the
maximum version (<literal><2</literal>)
was added by the system.
</para>
</listitem>
<listitem>
<para>Current release without a maximum
version specified
<programlisting>
$this->requires = array( 'MantisCore' => '2.0' );
</programlisting>
The plugin is compatible with MantisBT
>= 2.0 and < 3.0 (the latter is implicit);
code supporting older releases (e.g. 1.3)
must be maintained separately (i.e. in a
different branch).
</para>
</listitem>
<listitem>
<para>Only specify a maximum version
<programlisting>
$this->requires = array( 'MantisCore' => '< 3.1' );
</programlisting>
The plugin is compatible up to MantisBT
3.1 (not inclusive).
</para>
</listitem>
<listitem>
<para>Old release with a maximum version
<programlisting>
$this->requires = array( 'MantisCore' => '1.3, < 4.0' );
</programlisting>
The plugin is compatible with MantisBT
>= 1.3 and < 4.0.
</para>
</listitem>
</itemizedlist>
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>uses</term>
<listitem>
<para>An array of key/value pairs of
basename/version optional (soft) plugin dependencies.
See <literal>requires</literal> above
for details on how to specify versions.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>author</term>
<listitem>
<para>Your name, or an array of names.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>contact</term>
<listitem>
<para>An email address where you can be contacted.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>url</term>
<listitem>
<para>A web address for your plugin.</para>
</listitem>
</varlistentry>
</variablelist>
</section>
<section id="dev.plugins.building.pages">
<title>Pages and Files</title>
<para>
The plugin API provides a standard hierarchy and process for adding new pages and
files to your plugin. For strict definitions, pages are PHP files that will be
executed within the MantisBT core system, while files are defined as a separate
set of raw data that will be passed to the client's browser exactly as it appears
in the filesystem.
</para>
<para>
New pages for your plugin should be placed in your plugin's
<filename>pages/</filename> directory, and should be named using only letters and
numbers, and must have a ".php" file extension. To generate a URI to the new page
in MantisBT, the API function <function>plugin_page()</function> should be used.
Our Example plugin will create a page named <filename>foo.php</filename>, which
can then be accessed via <literal>plugin_page.php?page=Example/foo</literal>, the
same URI that <function>plugin_page()</function> would have generated:
</para>
<programlisting><filename>Example/pages/foo.php</filename>
<?php
echo '<p>Here is a link to <a href="', plugin_page( 'foo' ), '">page foo</a>.</p>';
</programlisting>
<para>
Adding non-PHP files, such as images or CSS stylesheets, follows a very similar
pattern as pages. Files should be placed in the plugin's
<filename>files/</filename> directory, and can only contain a single period in
the name. The file's URI is generated with the <function>plugin_file()</function>
function. For our Example plugin, we'll create a basic CSS stylesheet, and modify
the previously shown page to include the stylesheet:
</para>
<programlisting><filename>Example/files/foo.css</filename>
p.foo {
color: red;
}
</programlisting>
<programlisting><filename>Example/pages/foo.php</filename>
<?php
echo '<p>Here is a link to <a href="', plugin_page( 'foo' ), '">page foo</a>.</p>';
echo '<link rel="stylesheet" type="text/css" href="', plugin_file( 'foo.css' ), '"/>',
'<p class="foo">This is red text.</p>';
</programlisting>
<para>
Note that while <function>plugin_page()</function> expects only the page's name
without the extension, <function>plugin_file()</function> requires the entire
filename so that it can distinguish between <filename>foo.css</filename> and
a potential file <filename>foo.png</filename>.
</para>
<para>
The plugin's filesystem structure at this point looks like this:
</para>
<programlisting>
Example/
Example.php
pages/
foo.php
files/
foo.css
</programlisting>
</section>
<section id="dev.plugins.building.events">
<title>Events</title>
<para>
Plugins have an integrated method for both declaring and hooking events, without
needing to directly call the event API functions. These take the form of class
methods on your plugin.
</para>
<para>
To declare a new event, or a set of events, that your plugin will trigger, override
the <function>events()</function> method of your plugin class, and return an
associative array with event names as the key, and the event type as the value.
Let's add an event "foo" to our Example plugin that does not expect a return value
(an "execute" event type), and another event 'bar' that expects a single value that
gets modified by each hooked function (a "chain" event type):
</para>
<programlisting><filename>Example/Example.php</filename>
<?php
class ExamplePlugin extends MantisPlugin {
...
function events() {
return array(
'EVENT_EXAMPLE_FOO' => EVENT_TYPE_EXECUTE,
'EVENT_EXAMPLE_BAR' => EVENT_TYPE_CHAIN,
);
}
}
</programlisting>
<para>
When the Example plugin is loaded, the event system in MantisBT will add these two
events to its list of events, and will then allow other plugins or functions to hook
them. Naming the events "EVENT_PLUGINNAME_EVENTNAME" is not necessary, but is considered
best practice to avoid conflicts between plugins.
</para>
<para>
Hooking other events (or events from your own plugin) is almost identical to declaring
them. Instead of passing an event type as the value, your plugin must pass the name
of a class method on your plugin that will be called when the event is triggered. For
our Example plugin, we'll create a <function>foo()</function> and
<function>bar()</function> method on our plugin class, and hook them to the events we
declared earlier.
</para>
<programlisting><filename>Example/Example.php</filename>
<?php
class ExamplePlugin extends MantisPlugin {
...
function hooks() {
return array(
'EVENT_EXAMPLE_FOO' => 'foo',
'EVENT_EXAMPLE_BAR' => 'bar',
);
}
function foo( $p_event ) {
...
}
function bar( $p_event, $p_chained_param ) {
...
return $p_chained_param;
}
}
</programlisting>
<para>
Note that both hooked methods need to accept the <parameter>$p_event</parameter>
parameter, as that contains the event name triggering the method (for cases where
you may want a method hooked to multiple events). The <function>bar()</function>
method also accepts and returns the chained parameter in order to match the
expectations of the "bar" event.
</para>
<para>
Now that we have our plugin's events declared and hooked, let's modify our earlier
page so that triggers the events, and add some real processing to the hooked
methods:
</para>
<programlisting><filename>Example/Example.php</filename>
<?php
class ExamplePlugin extends MantisPlugin {
...
function foo( $p_event ) {
echo 'In method foo(). ';
}
function bar( $p_event, $p_chained_param ) {
return str_replace( 'foo', 'bar', $p_chained_param );
}
}
</programlisting>
<programlisting><filename>Example/pages/foo.php</filename>
<?php
echo '<p>Here is a link to <a href="', plugin_page( 'foo' ), '">page foo</a>.</p>';
'<link rel="stylesheet" type="text/css" href="', plugin_file( 'foo.css' ), '"/>',
'<p class="foo">';
event_signal( 'EVENT_EXAMPLE_FOO' );
$t_string = 'A sentence with the word "foo" in it.';
$t_new_string = event_signal( 'EVENT_EXAMPLE_BAR', array( $t_string ) );
echo $t_new_string, '</p>';
</programlisting>
<para>
When the first event "foo" is signaled, the Example plugin's
<function>foo()</function> method will execute and echo a string. After that,
the second event "bar" is signaled, and the page passes a string parameter; the
plugin's <function>bar()</function> gets the string and replaces any instance of
"foo" with "bar", and returns the resulting string. If any other plugin had
hooked the event, that plugin could have further modified the new string from the
Example plugin, or vice versa, depending on the loading order of plugins. The
page then echos the modified string that was returned from the event.
</para>
</section>
<section id="dev.plugins.building.config">
<title>Configuration</title>
<para>
Similar to events, plugins have a simplified method for declaring configuration
options, as well as API functions for retrieving or setting those values at runtime.
</para>
<para>
Declaring a new configuration option is achieved just like declaring events. By
overriding the <function>config()</function> method on your plugin class, your
plugin can return an associative array of configuration options, with the option
name as the key, and the default option as the array value. Our Example plugin
will declare an option "foo_or_bar", with a default value of "foo":
</para>
<programlisting><filename>Example/Example.php</filename>
<?php
class ExamplePlugin extends MantisPlugin {
...
function config() {
return array(
'foo_or_bar' => 'foo',
);
}
}
</programlisting>
<para>
Retrieving the current value of a plugin's configuration option is achieved by
using the plugin API's <function>plugin_config_get()</function> function, and can
be set to a modified value in the database using
<function>plugin_config_set()</function>. With these functions, the config option
is prefixed with the plugin's name, in attempt to automatically avoid conflicts in
naming. Our Example plugin will demonstrate this by adding a secure form to the
"config_page", and handling the form on a separate page "config_update" that will
modify the value in the database, and redirect back to page "config_page", just
like any other form and action page in MantisBT:
</para>
<programlisting><filename>Example/pages/config_page.php</filename>
<form action="<?php echo plugin_page( 'config_update' ) ?>" method="post">
<?php echo form_security_field( 'plugin_Example_config_update' ) ?>
<label>Foo or Bar?<br/><input name="foo_or_bar" value="<?php echo string_attribute( $t_foo_or_bar ) ?>"/></label>
<br/>
<label><input type="checkbox" name="reset"/> Reset</label>
<br/>
<input type="submit"/>
</form>
</programlisting>
<programlisting><filename>Example/pages/config_update.php</filename>
<?php
form_security_validate( 'plugin_Example_config_update' );
$f_foo_or_bar = gpc_get_string( 'foo_or_bar' );
$f_reset = gpc_get_bool( 'reset', false );
if( $f_reset ) {
plugin_config_delete( 'foo_or_bar' );
} else {
if( $f_foo_or_bar == 'foo' || $f_foo_or_bar == 'bar' ) {
plugin_config_set( 'foo_or_bar', $f_foo_or_bar );
}
}
form_security_purge( 'plugin_Example_config_update' );
print_successful_redirect( plugin_page( 'foo', true ) );
</programlisting>
<para>
Note that the <function>form_security_*()</function> functions are part of the
form API, and prevent CSRF attacks against forms that make changes to the system.
</para>
</section>
<section id="dev.plugins.building.language">
<title>Language and Localization</title>
<para>
MantisBT has a very advanced set of localization tools, which allow all parts of of
the application to be localized to the user's preferred language. This feature has
been extended for use by plugins as well, so that a plugin can be localized in much
the same method as used for the core system. Localizing a plugin involves creating
a language file for each localization available, and using a special API call to
retrieve the appropriate string for the user's language.
</para>
<para>
All language files for plugins follow the same format used in the core of MantisBT,
should be placed in the plugin's <filename>lang/</filename> directory, and named
the same as the core language files. Strings specific to the plugin should be
"namespaced" in a way that will minimize any risk of collision. Translating the
plugin to other languages already supported by MantisBT is then as simple as
creating a new strings file with the localized content; the MantisBT core will find
and use the new language strings automatically.
</para>
<para>
We'll use the "configuration" pages from the previous examples, and dress them up
with localized language strings, and add a few more flourishes to make the page act
like a standard MantisBT page. First we need to create a language file for English,
the default language of MantisBT and the default fallback language in the case that
some strings have not yet been localized to the user's language:
</para>
<programlisting><filename>Example/lang/strings_english.txt</filename>
<?php
$s_plugin_Example_configuration = "Configuration";
$s_plugin_Example_foo_or_bar = "Foo or Bar?";
$s_plugin_Example_reset = "Reset Value";
</programlisting>
<programlisting><filename>Example/pages/config_page.php</filename>
<?php
layout_page_header( plugin_lang_get( 'configuration' ) );
layout_page_begin();
$t_foo_or_bar = plugin_config_get( 'foo_or_bar' );
?>
<br/>
<form action="<?php echo plugin_page( 'config_update' ) ?>" method="post">
<?php echo form_security_field( 'plugin_Example_config_update' ) ?>
<table class="width60">
<tr>
<td class="form-title" rowspan="2"><?php echo plugin_lang_get( 'configuration' ) ?></td>
</tr>
<tr <?php echo helper_alternate_class() ?>>
<td class="category"><php echo plugin_lang_get( 'foo_or_bar' ) ?></td>
<td><input name="foo_or_bar" value="<?php echo string_attribute( $t_foo_or_bar ) ?>"/></td>
</tr>
<tr <?php echo helper_alternate_class() ?>>
<td class="category"><php echo plugin_lang_get( 'reset' ) ?></td>
<td><input type="checkbox" name="reset"/></td>
</tr>
<tr>
<td class="center" rowspan="2"><input type="submit"/></td>
</tr>
</table>
</form>
<?php
layout_page_end();
</programlisting>
<para>
The two calls to <function>layout_page_being()</function> and
<function>layout_page_end()</function> trigger the standard MantisBT header and
footer portions, respectively, which also displays things such as the menus and
triggers other layout-related events. <function>layout_page_header()</function>
pulls in the CSS classes for alternating row colors in the table. The rest of the
HTML and CSS follows the "standard" MantisBT markup styles for content and layout.
</para>
</section>
</section>