-
Notifications
You must be signed in to change notification settings - Fork 8
/
TUTORIAL.txt
1204 lines (919 loc) · 48 KB
/
TUTORIAL.txt
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
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
This tutorial is intended for PHP developers who are just getting started on working with
Envaya's source code. It assumes a working knowledge of PHP and HTML, but does not require
any particular pre-existing knowledge of Envaya's source code.
The tutorial also assumes that you already have successfully installed Envaya's
source code on your own development computer, and have installed the test data set
via scripts/install_test_data.php . See INSTALL.txt if you haven't already done that.
Each section of this tutorial builds on the result from previous sections, so they should
be completed in order.
=================
Table of Contents
=================
1. Adding a new page
2. Drawing a page in a layout
3. Internationalizing a page
4. Rendering HTML views
5. Adding parameters to translated strings
6. Fetching objects from the database
7. Creating an HTML form
8. Processing and saving form input
9. Restricting access to authorized users
10. Defining a new module
11. Adding a page under users' websites
12. Extending existing views from within modules
13. Defining a new Widget for users' websites
Todo:
14. Defining a new Model class and SQL schema
15. Customizing the mobile view
16. Using Envaya's Javascript libraries
17. Adding a new Javascript file
18. Creating a Selenium test
====================
1. Adding a New Page
====================
Your first task is to add a page on your local version of Envaya that displays "Hello World".
We will add this page at the URL "/pg/hello". Verify that this page does not already exist by opening
http://localhost/pg/hello in your web browser. You should see Envaya's custom 404 page, with
the message "Page not found".
In order to add a page at "/pg/hello", you need a basic understanding of how Envaya maps URLs to
actions, and how Envaya's PHP code is organized.
First, the web server (Apache, Nginx, etc) sends all requests to Envaya URLs
(except for static files) to www/index.php. This PHP file simply creates a Controller of type
Controller_Default, and then calls the execute() method, which finds the action for the requested
URL and performs that action.
To find Controller_Default, you need to know a little about how Envaya's PHP code is organized.
Almost all PHP classes in Envaya are defined in the engine/ directory. In the engine/ directory,
each PHP class is defined in its own file, and the PHP file is automatically loaded the first
time the class is referenced. The name of the file is determined by converting the name of the class
to lowercase, replacing _ with /, and appending .php.
By this convention, the class Controller_Default is defined in engine/controller/default.php.
Open this file.
At the top of Controller_Default is an array $routes. In any subclass of Controller,
the array $routes defines how URLs are mapped to actions (functions defined in the current
controller class) or other Controller classes.
In Controller_Default, note the following regular expression:
'regex' => '/(?P<controller>pg|admin)\b',
In the regular expression, the named group ?P<controller> means that a URL starting with "/pg"
will be mapped to a controller class named Controller_Pg, and a URL starting with "/admin" will
be mapped to a controller class named Controller_Admin.
When adding a new action, it is often easier to add it to an existing Controller class rather
than creating a new Controller class. This is why we suggested the URL "/pg/hello" -- because
there is already a controller that handles URLs starting with "/pg".
Open Controller_Pg, which by the same naming convention discussed before, is located at
engine/controller/pg.php.
Unlike Controller_Default, which forwards all URLs to another controller class, the $routes defined
in Controller_Pg allow you to define URLs by simply creating a function within that class, with the
prefix 'action_'.
So to create a new page at the URL "/pg/hello", simply add an empty function to the Controller_Pg
class named "action_hello":
function action_hello()
{
}
Refresh http://localhost/pg/hello in your browser. Instead of a 404 message, you should now
see a blank page.
Finally, we need to output "Hello World". Although "echo" would technically work here,
Envaya's controller actions should instead output their response using the methods defined in the
base Controller class (engine/controller.php).
The most basic method for outputting a response is 'set_content'. In Controller_Pg::action_hello,
add the following line:
$this->set_content("Hello World");
Refresh http://localhost/pg/hello in your browser. You should see "Hello World".
=============================
2. Drawing a Page in a Layout
=============================
Most pages on Envaya use a layout with navigation and language controls in the header or footer,
and a theme with particular CSS and background elements.
In this step, you will update /pg/hello to use a layout/theme so that it looks more like a normal
Envaya page.
The base Controller class has a method 'page_draw', which can be used instead of 'set_content' to
display a page's content within a pre-defined layout. In Controller_Pg::action_hello, remove the
'set_content' line and add:
$this->page_draw(array(
'content' => "Hello World",
));
Now refresh http://localhost/pg/hello in your browser. You should see something that looks
more like a normal Envaya page. But it looks wrong, because it doesn't have a title.
Change the call to 'page_draw' to add a 'title' parameter:
$this->page_draw(array(
'title' => "Hello!",
'content' => "Hello World",
));
By default, 'page_draw' uses a theme named "simple", defined in the themes/ directory.
Change the theme to use the "editor" theme instead, by adding a 'theme_name' parameter
when calling 'page_draw':
$this->page_draw(array(
'title' => "Hello!",
'content' => "Hello World",
'theme_name' => 'editor',
));
Now refresh http://localhost/pg/hello in your browser to see the page with the new theme.
Now that your page is generated via 'page_draw', it's already optimized for mobile devices!
At the bottom of the page, click "Mobile" to see a mobile-optimized version of the
"Hello World" page. Click "Standard" to switch back to the original view.
============================
3. Internationalizing a Page
============================
Envaya's page layout also lets the user switch between different languages.
However, if you try switching the language for /pg/hello, "Hello World"
will not be translated because it is currently hardcoded into the PHP source code.
In this step we will translate the "Hello World" page into English and Kiswahili.
In lib/util.php, Envaya defines a function named '__'. The parameter to this function
is a string called a "language key", which is an internal string identifier that
represents a particular phrase in Envaya's interface. The '__' function returns
the phrase translated into the user's language.
Change the Controller_Pg::action_hello method to use the __ function, and reference
some new language keys:
$this->page_draw(array(
'title' => __('tutorial:hello_title'),
'content' => __('tutorial:hello_content'),
'theme_name' => 'editor',
));
Refresh http://localhost/pg/hello in your browser. It should just display "tutorial:hello_content"
and "tutorial:hello_title" because the translations haven't been defined yet.
Translations for Envaya's interface are defined in PHP files in the languages/ directory.
English translations are defined in languages/en/; Kiswahili translations are defined
in languages/sw/, etc. Each PHP file just returns a associative array that maps
language keys to translated phrases. See languages/en/en_default.php for an example.
Because Envaya's interface has hundreds of different phrases, our translations are
split into many different files. When a particular language key is referenced,
Envaya determines which file(s) to search in based on the part of language key before
a ":" (colon) character.
For example, if the language key is "tutorial:hello_title", and the user's current language
is English, Envaya would search for that key in languages/en/en_tutorial.php
(if it was not already found in languages/en/en_default.php).
Create a new file languages/en/en_tutorial.php, and define the English translations for the
language keys defined earlier:
<?php
return array(
'tutorial:hello_title' => "Hello!",
'tutorial:hello_content' => "Hello World",
);
Also create a new file languages/sw/sw_tutorial.php to define some Kiswahili translations:
<?php
return array(
'tutorial:hello_title' => "Mambo!",
'tutorial:hello_content' => "Mambo Dunia",
);
Now reload http://localhost/pg/hello in your browser. Switch the language between English and
Kiswahili, and the appropriate translations should be displayed.
=======================
4. Rendering HTML views
=======================
In this step, we will make /pg/hello more interesting by adding HTML, and
saying "Hello" to a particular user.
In Envaya's coding conventions, HTML is generated by PHP files in the views/ directory,
not directly by embedding HTML into functions in the engine/ or lib/ directories.
Envaya's view() function includes a PHP file from the views/ directory, passing it some parameters,
and then returns the output of that PHP file as a string. The first argument to the view()
function is the name of the view. The path to the PHP file is generated from both the name of the
view and the current "view type" (e.g. 'default' for the standard HTML view, 'mobile' for the
mobile web view, 'rss' for the RSS view).
For example, if view name is 'tutorial/hello', and the user is using the standard HTML view, the
view will be loaded from the PHP file at 'views/default/tutorial/hello.php'.
Change the Controller_Pg::action_hello method to use the view function, and reference the
'tutorial/hello' view to generate the content:
$this->page_draw(array(
'title' => __('tutorial:hello_title'),
'content' => view('tutorial/hello'),
'theme_name' => 'editor',
));
Reload http://localhost/pg/hello in your browser.
You should get an error message: "view tutorial/hello does not exist".
Now create views/default/tutorial/hello.php (you will also need to create the
views/default/tutorial/ directory), and add the following content:
<?php
echo __('tutorial:hello_content');
?>
Used within view files, 'echo' adds a string to the output returned by the 'view' function.
Reload http://localhost/pg/hello in your browser. You should see the translated
version of "Hello World" as it was before.
In view files, you can switch between PHP and raw HTML as convenient. For example,
add the following content to views/default/tutorial/hello.php:
<ul>
<li><a href='/pg/feed'><?php echo __('feed:title'); ?></a></li>
<li><a href='/pg/search'><?php echo __('search'); ?></a></li>
</ul>
Reload http://localhost/pg/hello, and you should see links to the Latest updates
and Search pages.
Each view can take its own set of parameters. Let's take a parameter from the query
string and pass it into the view. Change Controller_Pg::action_hello as follows:
$username = get_input('username');
$this->page_draw(array(
'title' => __('tutorial:hello_title'),
'content' => view('tutorial/hello', array('username' => $username)),
'theme_name' => 'editor',
));
The get_input() method returns a parameter from $_GET or $_POST. Above, get_input('username')
returns the parameter named 'username'. For example, if the URL is
http://localhost/pg/hello?username=testorg, get_input('username') will return "testorg".
The expression "view('tutorial/hello', array('username' => $username))" renders the tutorial/hello
view with a parameter named 'username', set to the username from the $_GET or $_POST input.
Inside the view, the parameter can be accessed as part of the variable named $vars.
Copy the following content to views/default/tutorial/hello.php:
<?php
$username = $vars['username'] ?: 'somebody';
echo "Hello, $username!";
?>
Refresh http://localhost/pg/hello . It should say:
Hello, somebody!
Now open http://localhost/pg/hello?username=testorg . It should say:
Hello, testorg!
Now open http://localhost/pg/hello?username=%3Cscript%3Ealert%28%27hacked!%27%29%3C/script%3E .
In browsers that don't have built-in cross site scripting protection, the browser will
display an alert message! Other browsers may just display "Hello, !".
This code has a security flaw because user input is displayed in HTML without escaping.
Any attacker could easily generate a URL that they could use to steal the user's session
and gain full control of the user's account, or redirect the user to a malicious page.
To fix this bug, we need to escape any strings that could possibly contain untrusted HTML.
In Envaya, this can be done via the 'escape' function. Copy the following content to
views/default/tutorial/hello.php:
<?php
$username = $vars['username'] ?: 'somebody';
echo "Hello, ".escape($username)."!";
?>
Now open http://localhost/pg/hello?username=%3Cscript%3Ealert%28%27hacked!%27%29%3C/script%3E again.
It should say:
Hello, <script>alert('hacked!')</script>!
By using the escape() function whenever outputting untrusted text, malicious HTML is escaped
properly and cannot cause a cross-site scripting attack.
==========================================
5. Adding parameters to translated strings
==========================================
The previous version of hello.php hardcodes the phrase "Hello, ".escape($username)."!"
in English. We need to update our translations to allow displaying this phrase in
different languages.
In languages/en/en_tutorial.php, add:
'tutorial:hello_user' => "Hello, {name}!",
In languages/sw/sw_tutorial.php, add:
'tutorial:hello_user' => "Mambo, {name}!",
PHP's built-in 'strtr' function makes it easy to replace parameters in translated strings.
Copy the following content to views/default/tutorial/hello.php:
<?php
$username = $vars['username'] ?: 'somebody';
echo strtr(__('tutorial:hello_user'), array(
'{name}' => escape($username)
));
?>
Now open http://localhost/pg/hello?username=testorg , and change the language to Kiswahili.
It should say:
Mambo, testorg!
Sometimes, Envaya also uses PHP's 'sprintf' function, with unnamed parameters like '%s'.
However, it is often clearer to use named parameters.
=====================================
6. Fetching objects from the database
=====================================
The next task is to display the actual name of the user, rather than just their username.
To do this, you will need to fetch the User object from the database.
The User class, as you might expect, is defined in engine/user.php. In this file, you can see
that the User class has a static method User::get_by_username, which fetches the user object
from the database with a particular username, returning null if no user has that username.
You can also see that the User class has an attribute 'name' (specified as part of $table_attributes),
which is the display name of that user.
Since the user name also may contain untrusted HTML, it is also necessary to escape it.
Copy the following content to views/default/tutorial/hello.php:
<?php
$username = $vars['username'];
$user = User::get_by_username($username);
if ($user)
{
echo strtr(__('tutorial:hello_user'), array(
'{name}' => escape($user->name)
));
}
else
{
echo "no user found";
}
?>
Now reload http://localhost/pg/hello?username=testorg . It should say:
Hello, Test Org!
Although 'get_by_username' is specific to the User class, you can also query
objects from the database in a more generic way using the 'query' method.
Any database model (subclass of Model, defined in engine/model.php) defines
a static 'query' method, which returns a Query_Select object (defined in engine/query/select.php).
The Query_Select method makes it easy to construct and execute SELECT queries in SQL.
For example:
$user = User::query()->where('username = ?', $username)->get();
would return the same result as:
$user = User::get_by_username($username);
Query_Select uses prepared statements, with parameters replaced by '?' and
passed as separate parameters. This prevents against SQL injection attacks.
For example, the following statement would be vulnerable to SQL injection attacks:
$user = User::query()->where("username = '$username'")->get(); // BAD
This is a problem because $username could include an apostrophe character,
followed by malicious SQL that could drop tables, change passwords, etc.
To avoid this, always use '?' and pass parameters separately when calling where().
To get a list of all User objects matching a query, use filter().
Copy the following content to views/default/tutorial/hello.php:
<?php
$username = $vars['username'];
$user = User::get_by_username($username);
if ($user)
{
echo strtr(__('tutorial:hello_user'), array(
'{name}' => escape($user->name)
));
}
else
{
echo "invalid username. valid usernames are:<br /><br />";
$users = User::query()
->where('time_created < ?', time() - 60) // older than one minute
->order_by('username')
->filter();
foreach ($users as $user)
{
echo escape($user->username)."<br />";
}
}
?>
Now open http://localhost/pg/hello . It should say something like:
invalid username. valid usernames are:
testadmin
testorg
testposter0
testposter1
....
========================
7. Creating an HTML form
========================
In this step you will create a simple HTML form to gather input from the user.
This form will allow the user to change a particular user's email address.
Each view within Envaya can render other views with their own parameters.
Views with names like "input/...", which refer to input fields displayed
as part of HTML forms, are commonly included by other views.
For example, the view 'input/text' will render an HTML element like
"<input type='text' ... />", and 'input/submit' will render a HTML
<button type='submit'> element.
For "input/" views, the parameters are mostly the same as the standard
HTML attribute names. But in general it is necessary to look at each particular
view file in order to see what parameters it uses.
Open views/default/input/text.php. You can see that it accepts parameters named
'name', 'value', 'id', 'class', 'style', etc., from the standard HTML attributes,
as well as an Envaya-specific boolean parameter named 'track_dirty'. (In this particular
view file, it is somewhat difficult to see what parameters are used. The PHP built-in function
extract($vars) extracts $vars['name'] into $name, $vars['value'] into $value, etc.)
Copy the following content to views/default/tutorial/hello.php:
<?php
$username = $vars['username'];
$user = User::get_by_username($username);
if ($user)
{
echo "<form method='POST' action='/pg/hello?username={$user->username}'>";
echo "<label>New email address for ".escape($user->name).":</label>";
echo view('input/text', array('name' => 'email', 'value' => $user->email));
echo view('input/submit', array('value' => __('savechanges')));
echo "</form>";
}
Now open http://localhost/pg/hello?username=testorg . It should say:
New email address for Test Org:
[<current email address>]
[Save changes]
If you click the Save Changes button, it should submit the form,
and the page will be reloaded, but the changes will not actually be saved.
(It may appear that the email address has changed, but it will be reset
if you reload the page without resubmitting the form.)
===================================
8. Processing and Saving Form Input
===================================
Now, we will change the controller action for /pg/hello so that it actually
updates the selected user's email address.
Typically, for any pages containing forms, the controller action function
will delegate the work to an Action subclass. The Action base class
(engine/action.php) allows subclasses to define a method render(), which
handles GET requests; and process_input(), which handles POST requests.
Create a new PHP class engine/action/hello.php, and copy the following code:
<?php
class Action_Hello extends Action
{
function render()
{
$username = get_input('username');
$this->page_draw(array(
'title' => __('tutorial:hello_title'),
'content' => view('tutorial/hello', array('username' => $username)),
'theme_name' => 'editor',
));
}
}
Note that Action_Hello::render() has exactly the same code as what was in
Controller_Pg::action_hello(). Even the method 'page_draw', which is a Controller
method, can be called directly from within the Action_Hello class.
In engine/controller/pg.php, change Controller_Pg::action_hello to execute the
Action_Hello class:
$action = new Action_Hello($this);
$action->execute();
Now open http://localhost/pg/hello?username=testorg . It should display the form
the same as before.
Now click Save Changes. It should show an error message:
The page you were using has expired. Please try again.
This error message occurs as a result of a security check in the base Action class.
To prevent against cross-site request forgery attacks (CSRF), each <form> tag with
method='POST' must contain a security token to ensure that the form cannot be
submitted to Envaya from a third-party site without the user's permission.
To add this security token, add the following code to views/default/tutorial/hello.php
somewhere between the <form> and </form> tags:
echo view('input/securitytoken');
Now reload http://localhost/pg/hello?username=testorg and click "Save Changes".
It should display a blank white page.
It displays a blank page because you have not yet defined how the Action_Hello
class should handle POST requests.
In Action_Hello, add a process_input method:
function process_input()
{
$username = get_input('username');
$user = User::get_by_username($username);
$user->email = get_input('email');
$user->save();
SessionMessages::add("Email address saved!");
$this->redirect();
}
The above code introduces a few new methods:
Model::save() saves any subclass of Model to the database.
If the object was not already saved, it will execute a INSERT statement
in SQL, otherwise it will do an UPDATE statement.
SessionMessages::add($msg) displays a message to the user at the top of
the next page that they view. This is generally used for displaying 'positive'
messages, while SessionMessages::add_error is used for displaying error
messages.
Controller::redirect($url) redirects the user to another URL.
Without any arguments, it redirects the user to the current page (but as a
GET request instead of a POST request). This is commonly used at the
end of an action.
Another common requirement of processing forms is to validate user input,
to prevent PHP errors and prevent invalid input from being stored in the database.
In process_input, errors can be handled by throwing a ValidationException. When
process_input throws a ValidationException, an error message will be displayed
and render() will be called to show the form again.
Some methods are already defined within Envaya to validate particular common
input types. For example, 'validate_email_address' will throw a ValidationException
if an email address is invalid.
Update Action_Hello::process_input() as follows:
$username = get_input('username');
$user = User::get_by_username($username);
if (!$user)
{
throw new ValidationException("bad username");
}
$user->email = get_input('email');
validate_email_address($user->email);
$user->save();
SessionMessages::add("Email address saved!");
$this->redirect();
On http://localhost/pg/hello?username=testorg , enter the email address 'foo',
then click "Save Changes". It should display an error message:
The email address 'foo' does not appear to be a valid email address.
=========================================
9. Restricting access to authorized users
=========================================
Currently we have not implemented any access control, so anyone
can edit any user's email address, even without being logged in.
In this section, you will change Action_Hello so that users can change
only their own email address, except for administrators who can change
anyone's email address.
Action and Controller subclasses can define a method named 'before'.
For Action subclasses, 'before' is called before either 'render' or
'process_input'; for Controller subclasses, 'before' is called before
routing the request to any action or another controller. So 'before'
is generally a good place to put any code that performs access control.
You can also put code in 'before' that is common to both GET and POST
requests.
In Action_Hello, add a method:
function before()
{
$user = User::get_by_username(get_input('username'));
if (!$user)
{
throw new NotFoundException();
}
if (!$user->can_edit())
{
$this->force_login("You can't edit this user!");
}
}
The method 'can_edit' is defined in the Entity class (engine/entity.php),
which is the parent class of User. It tests if the currently logged in user
has permission to edit the given entity; for User objects, 'can_edit' only
returns true if the logged in user is the same user or an administrator.
(Other commonly used methods for access control include 'require_login'
and 'require_admin', defined in engine/controller.php.)
Now open http://localhost/pg/hello. You should get a custom 404 page
with the title "Page not found". This is a result of the line
"throw new NotFoundException();".
When you throw a NotFoundException, the current request is aborted
(without calling the render() or process_input() methods), and a 404 page
is displayed to the user.
Now open http://localhost/pg/hello?username=testorg . You should be
redirected to the login page with the message "You can't edit this user!".
This is a result of the call to 'force_login', which is defined in
engine/controller.php. Internally, force_login throws a RedirectException,
which aborts the current request and redirects the user to another URL
with an error message.
Enter the username "testorg", password "testtest" to log in. Now it should
display the page where you can enter a new email address for testorg.
One problem with the code in Action_Hello and the view tutorial/hello
is that code like "User::get_by_username(get_input('username'))" is now
repeated in three different places: the 'before' method, the 'process_input'
method, and in tutorial/hello.
So let's refactor this code so that this code only appears once. To do
this, the 'before' method can save the User object as a property of the
Action_Hello class. Update engine/action/hello.php like so:
<?php
class Action_Hello extends Action
{
private $user;
function before()
{
$user = User::get_by_username(get_input('username'));
if (!$user)
{
throw new NotFoundException();
}
if (!$user->can_edit())
{
$this->force_login("you can't edit this user!");
}
$this->user = $user;
}
function process_input()
{
$user = $this->user;
$user->email = get_input('email');
validate_email_address($user->email);
$user->save();
SessionMessages::add("Email address saved!");
$this->redirect();
}
function render()
{
$this->page_draw(array(
'title' => __('tutorial:hello_title'),
'content' => view('tutorial/hello', array('user' => $this->user)),
'theme_name' => 'editor',
));
}
}
Since we have just changed the parameter to tutorial/hello to be the user,
not the username, we also need to update views/default/tutorial/hello.php like so:
<?php
$user = $vars['user'];
echo "<form method='POST' action='/pg/hello?username={$user->username}'>";
echo view('input/securitytoken');
echo "<label>New email address for ".escape($user->name).":</label>";
echo view('input/text', array('name' => 'email', 'value' => $user->email));
echo view('input/submit', array('value' => __('savechanges')));
echo "</form>";
http://localhost/pg/hello?username=testorg should still work the same as before,
but without duplicated code.
=========================
10. Defining a new module
=========================
One problem with the way your new code is organized is that it is currently
mixed in with the 'core' Envaya code. Somebody else looking at Envaya's code
would have a difficult time telling where the 'core' code ended and the 'tutorial'
code began.
Envaya allows code for features to be organized inside modules. Each module allows
code for a particular independent feature to be grouped together, separate from
other modules and from the core Envaya code. Modules can also easily be enabled or
disabled via a configurating setting.
In this section, you will move all the tutorial code you have created into a
new module, named 'tutorial'.
In the mod/ directory, create a new directory named 'tutorial'. This directory name
is the name of your module.
In the tutorial directory, create an empty file named 'module.php', and add the following
content:
<?php
class Module_Tutorial extends Module
{
}
Every module must contain a file named module.php in its root directory that defines
a class named like "Module_<modulename>" that extends Module. We will edit this file shortly.
Next, in config/default.php, find a setting named 'modules'. This is the list of
Envaya modules that are enabled by default.
Copy this setting into config/local.php, and add the string 'tutorial' to the
end of the list. This enables the 'tutorial' module on your computer.
The directory structure for each module is analogous to the directory structure
under Envaya's root directory. For the tutorial module, we will have subdirectories
mod/tutorial/engine/, mod/tutorial/languages/, and mod/tutorial/views/.
Move the following files/directories, creating parent directories as needed:
engine/action/hello.php to mod/tutorial/engine/action/hello.php
views/default/tutorial/ to mod/tutorial/views/default/tutorial/
languages/en/en_tutorial.php to mod/tutorial/languages/en/en_tutorial.php
languages/sw/sw_tutorial.php to mod/tutorial/languages/sw/sw_tutorial.php
Whenever you move files between modules, or enable/disable modules, you may need
to run the following command to refresh the cache that Envaya uses to determine
which module contains a particular file:
php make.php path_cache
(If you don't run this command, you may get errors when trying to use any of the
files that were moved.)
Now refresh http://localhost/pg/hello?username=testorg . It should still work the same as before,
but without duplicated code.
There is still one problem: There is still code in the 'core' file engine/controller/pg.php
that refers to code in the 'tutorial' module. (If you run "php scripts/module_deps.php" on
the command line, it should print out that the '(core)' module references the 'tutorial'
module 1 time.)
The file mod/tutorial/module.php allows you to avoid directly referencing your module's code
from within core classes.
To move Controller_Pg::action_hello into a module, we first need to create a new Controller
subclass inside your module.
Create the file mod/tutorial/engine/controller/tutorial.php, with the following contents:
<?php
class Controller_Tutorial extends Controller
{
static $routes = array(
array(
'action' => 'action_hello',
)
);
function action_hello()
{
$action = new Action_Hello($this);
$action->execute();
}
}
In the code above, the $routes variable says that all URLs
are handled by the function 'action_hello'.
Remove the method action_hello from Controller_Pg.
Now update mod/tutorial/module.php so that /pg/hello
is routed to the Controller_Tutorial class:
<?php
class Module_Tutorial extends Module
{
static $autoload_patch = array(
'Controller_Pg',
);
static function patch_Controller_Pg()
{
Controller_Pg::$routes[] = array(
'regex' => '/hello\b',
'controller' => 'Controller_Tutorial',
);
}
}
The patch_Controller_Pg() function adds an entry to the $routes array of
Controller_Pg, associating the URL /hello with the controller named Controller_Tutorial.
By adding 'Controller_Pg' to the $autoload_patch array, the Module registers
the patch_Controller_Pg() function to be called whenever the Controller_Pg is loaded.
Many requests may not require Controller_Pg, so this structure allows Envaya to only
load the PHP code that is actually used.
Now refresh http://localhost/pg/hello?username=testorg . It should work the same
as before, but now all of the tutorial code is inside the 'tutorial' module. When you
finish this tutorial, you can simply remove 'tutorial' from the 'modules' array in
config/local.php, and none of the tutorial code will be used anymore.
=======================================
11. Adding a page under users' websites
=======================================
Currently this page to change a user's email address uses a query string parameter
to determine the user that is currently active. However, usually when you want to create
a page on Envaya specific to a particular user, you would just place it under that
user's Envaya website.
In other words, instead of a URL like this:
http://localhost/pg/hello?username=testorg
It would be better to use a URL like this:
http://localhost/testorg/hello
As you can see from Controller_Default (engine/controller/default.php), URLs starting
with a username are forwarded to the Controller_UserSite controller
(engine/controller/usersite.php).
To move the '/hello' route to the Controller_UserSite controller, modify
mod/tutorial/module.php to refer to Controller_UserSite instead of Controller_Pg:
<?php
class Module_Tutorial extends Module
{
static $autoload_patch = array(
'Controller_UserSite',
);
static function patch_Controller_UserSite()
{
array_unshift(Controller_UserSite::$routes[], array(
'regex' => '/hello\b',
'controller' => 'Controller_Tutorial',
));
}
}
In this case, we call array_unshift() to add the route at the beginning (0th index) of
the Controller_UserSite::$routes array instead of at the end.
This is necessary because Controller_UserSite already contains a route regex that would
match "/hello", so our regex needs to go before it.
Controller_UserSite extends Controller_User (engine/controller/user.php), which defines
a method get_user() that returns the User object associated with the username from the URL.
Controller_User also defines a method require_editor() that forwards the visitor to the login
page if the visitor cannot edit the current User object. It also uses the 'editor' layout
by default.
In order to use these methods in Action_Hello, its associated controller Controller_Tutorial
needs to extend Controller_User as well. Update the class declaration in
mod/tutorial/engine/controller/tutorial.php, like so:
class Controller_Tutorial extends Controller_User
Now, update and simplify the Action_Hello class in mod/tutorial/engine/action/hello.php, using
the methods from Controller_User:
<?php
class Action_Hello extends Action
{
function before()
{
$this->require_editor();
}
function process_input()
{
$user = $this->get_user();
$user->email = get_input('email');
validate_email_address($user->email);
$user->save();
SessionMessages::add("Email address saved!");
$this->redirect();
}
function render()
{
$this->page_draw(array(
'title' => __('tutorial:hello_title'),
'content' => view('tutorial/hello', array('user' => $this->get_user())),
));
}
}
Finally, update the 'tutorial/hello' view to use the new URL for the form action:
echo "<form method='POST' action='/{$user->username}/hello'>";
Now, open http://localhost/testorg/hello in your browser. It should work the same as
/pg/hello?username=testorg did before.
================================================
12. Extending existing views from within modules
================================================
Currently, the page to update a user's email address is not linked anywhere on Envaya;
you have to manually enter the URL in the address bar in order to see it.
In this section, you will add a link to your new page from an existing page on Envaya,
in particular, from the user's Settings page:
http://localhost/testorg/settings
Of course the Settings page already has a way to change the user's email address, but
for the purpose of this tutorial, just pretend that functionality doesn't already exist.
In order to add a link on this page, you first have to figure out which view is being
rendered that you could add a link to.
Since the URL of the Settings page is http://localhost/<username>/settings, the
Controller_UserSite controller handles this page. By examining the regular expressions in
Controller_UserSite::$routes, you can figure out that the page is handled by
Controller_UserSite::action_settings, which executes an Action_Settings action.
Action_Settings is defined in engine/action/settings.php, where you can see that
'account/settings' is the content view being rendered.
To modify a view like 'account/settings' from within a module, you will need to register
a 'patch' function for that view in your module's module.php file.
Add the following lines to the Module_Tutorial class in mod/tutorial/module.php:
static $view_patch = array(
'account/settings',
);