mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge remote-tracking branch 'origin/3.1'
Conflicts: api/RSSFeed.php
This commit is contained in:
commit
af96432c1e
@ -5,7 +5,6 @@ php:
|
||||
|
||||
env:
|
||||
- TESTDB=MYSQL
|
||||
- TESTDB=PGSQL
|
||||
- TESTDB=SQLITE
|
||||
|
||||
matrix:
|
||||
|
@ -13,7 +13,7 @@ class CMSBatchActionHandler extends RequestHandler {
|
||||
static $url_handlers = array(
|
||||
'$BatchAction/applicablepages' => 'handleApplicablePages',
|
||||
'$BatchAction/confirmation' => 'handleConfirmation',
|
||||
'$BatchAction' => 'handleAction',
|
||||
'$BatchAction' => 'handleBatchAction',
|
||||
);
|
||||
|
||||
protected $parentController;
|
||||
@ -66,7 +66,7 @@ class CMSBatchActionHandler extends RequestHandler {
|
||||
return Controller::join_links($this->parentController->Link(), $this->urlSegment);
|
||||
}
|
||||
|
||||
public function handleAction($request) {
|
||||
public function handleBatchAction($request) {
|
||||
// This method can't be called without ajax.
|
||||
if(!$request->isAjax()) {
|
||||
$this->parentController->redirectBack();
|
||||
|
@ -218,3 +218,9 @@ table.ss-gridfield-table tr.ss-gridfield-item.even { background: #F0F4F7; }
|
||||
.cms .cms-content-actions .Actions .action-menus.ss-ui-action-tabset ul.ui-tabs-nav .ui-state-active a.ui-tabs-anchor { background: transparent url(../images/sprites-32x32/arrow_up_lighter.png) no-repeat right top; }
|
||||
.cms .cms-content-actions .Actions .action-menus.ss-ui-action-tabset ul.ui-tabs-nav .ui-state-active a.ui-tabs-anchor:hover { background: transparent url(../images/sprites-32x32/arrow_up_darker.png) no-repeat right top; }
|
||||
.cms .cms-content-actions .Actions .action-menus.ss-ui-action-tabset .ui-tabs-panel button.ss-ui-button { width: 190px; /* Width 100% not calculating by ie7 */ }
|
||||
|
||||
/* Insert Media Area */
|
||||
.ui-dialog-titlebar { z-index: 100000; }
|
||||
|
||||
.ss-uploadfield-item-info .dimensions input { float: left; width: 150px; }
|
||||
.ss-uploadfield-item-info .dimensions .fieldgroup-field.last { margin-left: 16px; }
|
||||
|
@ -192,7 +192,7 @@ form.small .field input.text, form.small .field textarea, form.small .field sele
|
||||
.field .chzn-container-single .chzn-single div b { background-position: 4px 0px; }
|
||||
.field .chzn-choices { -webkit-border-radius: 3px; -moz-border-radius: 3px; -ms-border-radius: 3px; -o-border-radius: 3px; border-radius: 3px; }
|
||||
.field input.month, .field input.day, .field input.year { width: 56px; }
|
||||
.field input.time { width: 64px; }
|
||||
.field input.time { width: 88px; }
|
||||
.field.remove-splitter { border-bottom: none; box-shadow: none; }
|
||||
|
||||
/** ---------------------------------------------------- Buttons ---------------------------------------------------- */
|
||||
@ -212,6 +212,7 @@ form.small .field input.text, form.small .field textarea, form.small .field sele
|
||||
.cms .ss-ui-button { margin-top: 0px; font-weight: bold; text-decoration: none; line-height: 16px; color: #393939; border: 1px solid #c0c0c2; border-bottom: 1px solid #a6a6a9; cursor: pointer; background-color: #e6e6e6; white-space: nowrap; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2Q5ZDlkOSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background: -webkit-linear-gradient(#ffffff, #d9d9d9); background: -moz-linear-gradient(#ffffff, #d9d9d9); background: -o-linear-gradient(#ffffff, #d9d9d9); background: linear-gradient(#ffffff, #d9d9d9); text-shadow: white 0 1px 1px; /* constructive */ /* destructive */ }
|
||||
.cms .ss-ui-button.ui-state-hover, .cms .ss-ui-button:hover { text-decoration: none; background-color: white; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: -webkit-linear-gradient(#ffffff, #e6e6e6); background: -moz-linear-gradient(#ffffff, #e6e6e6); background: -o-linear-gradient(#ffffff, #e6e6e6); background: linear-gradient(#ffffff, #e6e6e6); -webkit-box-shadow: 0 0 5px #b3b3b3; -moz-box-shadow: 0 0 5px #b3b3b3; box-shadow: 0 0 5px #b3b3b3; }
|
||||
.cms .ss-ui-button:active, .cms .ss-ui-button:focus, .cms .ss-ui-button.ui-state-active, .cms .ss-ui-button.ui-state-focus { border: 1px solid #b3b3b3; background-color: white; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: -webkit-linear-gradient(#ffffff, #e6e6e6); background: -moz-linear-gradient(#ffffff, #e6e6e6); background: -o-linear-gradient(#ffffff, #e6e6e6); background: linear-gradient(#ffffff, #e6e6e6); -webkit-box-shadow: 0 0 5px #b3b3b3 inset; -moz-box-shadow: 0 0 5px #b3b3b3 inset; box-shadow: 0 0 5px #b3b3b3 inset; }
|
||||
.cms .ss-ui-button.ss-ui-action-minor span { padding-left: 0; padding-right: 0; }
|
||||
.cms .ss-ui-button.ss-ui-action-constructive { text-shadow: none; font-weight: bold; color: white; border-color: #1f9433; border-bottom-color: #166a24; background-color: #1f9433; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzkzYmU0MiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzFmOTQzMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #93be42), color-stop(100%, #1f9433)); background: -webkit-linear-gradient(#93be42, #1f9433); background: -moz-linear-gradient(#93be42, #1f9433); background: -o-linear-gradient(#93be42, #1f9433); background: linear-gradient(#93be42, #1f9433); text-shadow: #1c872f 0 -1px -1px; }
|
||||
.cms .ss-ui-button.ss-ui-action-constructive.ui-state-hover, .cms .ss-ui-button.ss-ui-action-constructive:hover { border-color: #166a24; background-color: #1f9433; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2E0Y2EzYSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzIzYTkzYSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #a4ca3a), color-stop(100%, #23a93a)); background: -webkit-linear-gradient(#a4ca3a, #23a93a); background: -moz-linear-gradient(#a4ca3a, #23a93a); background: -o-linear-gradient(#a4ca3a, #23a93a); background: linear-gradient(#a4ca3a, #23a93a); }
|
||||
.cms .ss-ui-button.ss-ui-action-constructive:active, .cms .ss-ui-button.ss-ui-action-constructive:focus, .cms .ss-ui-button.ss-ui-action-constructive.ui-state-active, .cms .ss-ui-button.ss-ui-action-constructive.ui-state-focus { background-color: #1d8c30; -webkit-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); -moz-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); }
|
||||
@ -230,7 +231,6 @@ form.small .field input.text, form.small .field textarea, form.small .field sele
|
||||
.fieldgroup .fieldgroup-field { float: left; display: block; padding: 8px 8px 0 0; }
|
||||
.fieldgroup .fieldgroup-field .field { border: none; padding-bottom: 0; }
|
||||
.fieldgroup .fieldgroup-field label { margin-left: 0; margin-right: 1em; width: auto; }
|
||||
.fieldgroup .fieldgroup-field input, .fieldgroup .fieldgroup-field textarea { float: right; }
|
||||
.fieldgroup.stacked .fieldgroup-field { float: none; }
|
||||
|
||||
.ss-toggle { margin: 8px 0; }
|
||||
@ -468,7 +468,7 @@ body.cms { overflow: hidden; }
|
||||
#PageType ul li input, #PageType ul li label, #PageType ul li .page-icon, #PageType ul li .title { float: left; line-height: 1.3em; }
|
||||
#PageType ul li .page-icon { margin: 0 4px; }
|
||||
#PageType ul li .title { width: 120px; font-weight: bold; padding-right: 10px; }
|
||||
#PageType ul li .description { font-style: italic; }
|
||||
#PageType ul li .description { font-style: italic; display: inline; clear: none; margin: 0; }
|
||||
|
||||
/** -------------------------------------------- Content toolbar -------------------------------------------- */
|
||||
.cms-content-toolbar { min-height: 29px; display: block; margin: 0 0 8px 0; border-bottom: 1px solid #d0d3d5; -webkit-box-shadow: 0 1px 0 rgba(248, 248, 248, 0.9); -moz-box-shadow: 0 1px 0 rgba(248, 248, 248, 0.9); -o-box-shadow: 0 1px 0 rgba(248, 248, 248, 0.9); box-shadow: 0 1px 0 rgba(248, 248, 248, 0.9); *zoom: 1; /* smaller treedropdown */ }
|
||||
@ -514,16 +514,19 @@ body.cms { overflow: hidden; }
|
||||
|
||||
/** CMS Batch actions */
|
||||
.cms-content-batchactions { float: left; position: relative; display: block; }
|
||||
.cms-content-batchactions .view-mode-batchactions-wrapper { float: left; padding: 4px 6px; border: 1px solid #aaa; margin-bottom: 8px; background-color: #D9D9D9; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2Q5ZDlkOSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background-image: -webkit-linear-gradient(top, #ffffff, #d9d9d9); background-image: -moz-linear-gradient(top, #ffffff, #d9d9d9); background-image: -o-linear-gradient(top, #ffffff, #d9d9d9); background-image: linear-gradient(top, #ffffff, #d9d9d9); border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
|
||||
.cms-content-batchactions .view-mode-batchactions-wrapper label { display: none; }
|
||||
.cms-content-batchactions .view-mode-batchactions-wrapper { height: 18px; float: left; padding: 4px 6px; border: 1px solid #aaa; margin-bottom: 8px; background-color: #D9D9D9; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2Q5ZDlkOSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background-image: -webkit-linear-gradient(top, #ffffff, #d9d9d9); background-image: -moz-linear-gradient(top, #ffffff, #d9d9d9); background-image: -o-linear-gradient(top, #ffffff, #d9d9d9); background-image: linear-gradient(top, #ffffff, #d9d9d9); border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
|
||||
.cms-content-batchactions .view-mode-batchactions-wrapper input { vertical-align: middle; }
|
||||
.cms-content-batchactions .view-mode-batchactions-wrapper label { vertical-align: middle; display: none; }
|
||||
.cms-content-batchactions .view-mode-batchactions-wrapper fieldset, .cms-content-batchactions .view-mode-batchactions-wrapper .Actions { display: inline-block; }
|
||||
.cms-content-batchactions.inactive .view-mode-batchactions-wrapper { border-radius: 4px; }
|
||||
.cms-content-batchactions.inactive .view-mode-batchactions-wrapper label { display: inline; }
|
||||
.cms-content-batchactions form > * { display: block; float: left; }
|
||||
.cms-content-batchactions form.cms-batch-actions { float: left; }
|
||||
.cms-content-batchactions.inactive form { display: none; }
|
||||
.cms-content-batchactions .chzn-container-single { display: block; }
|
||||
.cms-content-batchactions .chzn-container-single .chzn-single { margin-left: -1px; border-radius: 0; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2Q5ZDlkOSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background-image: -webkit-linear-gradient(top, #ffffff, #d9d9d9); background-image: -moz-linear-gradient(top, #ffffff, #d9d9d9); background-image: -o-linear-gradient(top, #ffffff, #d9d9d9); background-image: linear-gradient(top, #ffffff, #d9d9d9); }
|
||||
.cms-content-batchactions .cms-batch-actions .Actions .ss-ui-button { margin-left: -1px; border-top-left-radius: 0; border-bottom-left-radius: 0; }
|
||||
.cms-content-batchactions .chzn-container-single .chzn-single span { padding-top: 0; }
|
||||
.cms-content-batchactions .cms-batch-actions .Actions .ss-ui-button { padding-top: 4px; padding-bottom: 4px; height: 28px; margin-left: -1px; border-top-left-radius: 0; border-bottom-left-radius: 0; }
|
||||
.cms-content-batchactions .cms-batch-actions .Actions .ss-ui-button.ui-state-disabled { opacity: 1; color: #ccc; }
|
||||
|
||||
#Form_BatchActionsForm select { width: 200px; }
|
||||
@ -925,17 +928,17 @@ li.class-ErrorPage > a a .jstree-pageicon { background-position: 0 -112px; }
|
||||
.cms-preview.mobile .preview-scroll, .cms-preview.mobileLandscape .preview-scroll, .cms-preview.tablet .preview-scroll, .cms-preview.tabletLandscape .preview-scroll, .cms-preview.desktop .preview-scroll { background-color: #eceff1; /* cover website preview icon */ }
|
||||
.cms-preview.mobile .preview-scroll .preview-device-outer, .cms-preview.mobileLandscape .preview-scroll .preview-device-outer, .cms-preview.tablet .preview-scroll .preview-device-outer, .cms-preview.tabletLandscape .preview-scroll .preview-device-outer, .cms-preview.desktop .preview-scroll .preview-device-outer { -webkit-border-radius: 7px; -moz-border-radius: 7px; -ms-border-radius: 7px; -o-border-radius: 7px; border-radius: 7px; background: #d5dde2; border: 1px solid transparent; border-left: 1px solid #cfd9de; padding: 0 16px 16px; }
|
||||
.cms-preview.mobile .preview-scroll .preview-device-outer .preview-device-inner, .cms-preview.mobileLandscape .preview-scroll .preview-device-outer .preview-device-inner, .cms-preview.tablet .preview-scroll .preview-device-outer .preview-device-inner, .cms-preview.tabletLandscape .preview-scroll .preview-device-outer .preview-device-inner, .cms-preview.desktop .preview-scroll .preview-device-outer .preview-device-inner { border-top: 2px solid #e1e7ea; border-right: 1px solid transparent; border-bottom: 1px solid #e1e7ea; border-left: 1px solid #c3cfd6; }
|
||||
.cms-preview.mobile .preview-scroll .preview-device-outer { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); -ms-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); -webkit-transition: all 0.3s ease-in; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-in 1s; -o-transition: all 0.3s ease-in 1s; transition: all 0.3s ease-in 1s; height: 568px; margin: 20px auto 20px; overflow: hidden; padding-top: 16px; width: 335px; }
|
||||
.cms-preview.mobile .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); -ms-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; width: 335px; }
|
||||
.cms-preview.mobile .preview-scroll .preview-device-outer { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); -ms-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); -webkit-transition: all 0.3s ease-in; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-in 1s; -o-transition: all 0.3s ease-in 1s; transition: all 0.3s ease-in 1s; margin: 20px auto 20px; overflow: hidden; padding-top: 16px; }
|
||||
.cms-preview.mobile .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); -ms-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; }
|
||||
.cms-preview.mobile .preview-scroll .preview-device-outer.rotate { -webkit-transform: rotate(-90deg); -moz-transform: rotate(-90deg); -ms-transform: rotate(-90deg); -o-transform: rotate(-90deg); transform: rotate(-90deg); -webkit-transition: all 0.3s ease-in; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-in 1s; -o-transition: all 0.3s ease-in 1s; transition: all 0.3s ease-in 1s; height: 583px; margin: 0px auto 0px; width: 320px; }
|
||||
.cms-preview.mobile .preview-scroll .preview-device-outer.rotate .preview-device-inner { -webkit-transform-origin: 160px 160px; -moz-transform-origin: 160px 160px; -ms-transform-origin: 160px 160px; -o-transform-origin: 160px 160px; transform-origin: 160px 160px; -webkit-transform: rotate(90deg); -moz-transform: rotate(90deg); -ms-transform: rotate(90deg); -o-transform: rotate(90deg); transform: rotate(90deg); -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; height: 320px; width: 583px; }
|
||||
.cms-preview.mobileLandscape .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; height: 320px; margin: 12% auto; padding-top: 16px; width: 583px; }
|
||||
.cms-preview.mobileLandscape .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; width: 583px; }
|
||||
.cms-preview.tablet .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; height: 1024px; margin: 0 auto; width: 783px; }
|
||||
.cms-preview.tablet .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; width: 783px; }
|
||||
.cms-preview.tabletLandscape .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; height: 768px; margin: 0 auto; width: 1039px; }
|
||||
.cms-preview.tabletLandscape .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; width: 1039px; }
|
||||
.cms-preview.desktop .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; height: 800px; margin: 0 auto; width: 1024px; }
|
||||
.cms-preview.mobileLandscape .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; margin: 12% auto; padding-top: 16px; }
|
||||
.cms-preview.mobileLandscape .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; }
|
||||
.cms-preview.tablet .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; margin: 0 auto; }
|
||||
.cms-preview.tablet .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; }
|
||||
.cms-preview.tabletLandscape .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; margin: 0 auto; }
|
||||
.cms-preview.tabletLandscape .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; }
|
||||
.cms-preview.desktop .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; margin: 0 auto; }
|
||||
|
||||
/********************************************
|
||||
* Defines the styles for .ss-ui-action-tabset:
|
||||
|
@ -108,7 +108,10 @@
|
||||
// specifically opt-out of this behaviour via "data-skip-autofocus".
|
||||
// This opt-out is useful if the first visible field is shown far down a scrollable area,
|
||||
// for example for the pagination input field after a long GridField listing.
|
||||
this.find(':input:not(:submit)[data-skip-autofocus!="true"]').filter(':visible:first').focus();
|
||||
// Skip if an element in the form is already focused.
|
||||
if(!this.find(document.activeElement).length) {
|
||||
this.find(':input:not(:submit)[data-skip-autofocus!="true"]').filter(':visible:first').focus();
|
||||
}
|
||||
},
|
||||
onunmatch: function() {
|
||||
this._super();
|
||||
|
@ -17,7 +17,7 @@
|
||||
* The order is significant - if the state is not available, preview will start searching the list
|
||||
* from the beginning.
|
||||
*/
|
||||
AllowedStates: ['StageLink', 'LiveLink'],
|
||||
AllowedStates: ['StageLink', 'LiveLink','ArchiveLink'],
|
||||
|
||||
/**
|
||||
* API
|
||||
@ -36,12 +36,54 @@
|
||||
*/
|
||||
IsPreviewEnabled: false,
|
||||
|
||||
/**
|
||||
* Mode in which the preview will be enabled.
|
||||
*/
|
||||
DefaultMode: 'split',
|
||||
|
||||
Sizes: {
|
||||
auto: {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
mobile: {
|
||||
width: '335px', // add 15px for approx desktop scrollbar
|
||||
height: '568px'
|
||||
},
|
||||
mobileLandscape: {
|
||||
width: '583px', // add 15px for approx desktop scrollbar
|
||||
height: '320px'
|
||||
},
|
||||
tablet: {
|
||||
width: '783px', // add 15px for approx desktop scrollbar
|
||||
height: '1024px'
|
||||
},
|
||||
tabletLandscape: {
|
||||
width: '1039px', // add 15px for approx desktop scrollbar
|
||||
height: '768px'
|
||||
},
|
||||
desktop: {
|
||||
width: '1024px',
|
||||
height: '800px'
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* API
|
||||
* Switch the preview to different state.
|
||||
* stateName can be one of the "AllowedStates".
|
||||
*
|
||||
* @param {String}
|
||||
* @param {Boolean} Set to FALSE to avoid persisting the state
|
||||
*/
|
||||
changeState: function(stateName) {
|
||||
changeState: function(stateName, save) {
|
||||
var self = this, states = this._getNavigatorStates();
|
||||
if(save !== false) {
|
||||
$.each(states, function(index, state) {
|
||||
self.saveState('state', stateName);
|
||||
});
|
||||
}
|
||||
|
||||
this.setCurrentStateName(stateName);
|
||||
this._loadCurrentState();
|
||||
this.redraw();
|
||||
@ -54,17 +96,26 @@
|
||||
* Change the preview mode.
|
||||
* modeName can be: split, content, preview.
|
||||
*/
|
||||
changeMode: function(modeName) {
|
||||
changeMode: function(modeName, save) {
|
||||
var container = $('.cms-container');
|
||||
|
||||
if (modeName === 'split') {
|
||||
if (modeName == 'split') {
|
||||
container.entwine('.ss').splitViewMode();
|
||||
} else if (modeName === 'content') {
|
||||
this.setIsPreviewEnabled(true);
|
||||
this._loadCurrentState();
|
||||
} else if (modeName == 'content') {
|
||||
container.entwine('.ss').contentViewMode();
|
||||
} else {
|
||||
this.setIsPreviewEnabled(false);
|
||||
this._loadCurrentState();
|
||||
} else if (modeName == 'preview') {
|
||||
container.entwine('.ss').previewMode();
|
||||
this.setIsPreviewEnabled(true);
|
||||
} else {
|
||||
throw 'Invalid mode: ' + modeName;
|
||||
}
|
||||
|
||||
if(save !== false) this.saveState('mode', modeName);
|
||||
|
||||
this.redraw();
|
||||
|
||||
return this;
|
||||
@ -76,10 +127,17 @@
|
||||
* sizeName can be: auto, desktop, tablet, mobile.
|
||||
*/
|
||||
changeSize: function(sizeName) {
|
||||
this.setCurrentSizeName(sizeName);
|
||||
var sizes = this.getSizes();
|
||||
|
||||
this.removeClass('auto desktop tablet mobile')
|
||||
.addClass(sizeName);
|
||||
this.setCurrentSizeName(sizeName);
|
||||
this.removeClass('auto desktop tablet mobile').addClass(sizeName);
|
||||
this.find('.preview-device-outer')
|
||||
.width(sizes[sizeName].width)
|
||||
.height(sizes[sizeName].height);
|
||||
this.find('.preview-device-inner')
|
||||
.width(sizes[sizeName].width);
|
||||
|
||||
this.saveState('size', sizeName);
|
||||
|
||||
this.redraw();
|
||||
|
||||
@ -116,6 +174,24 @@
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the preview options for this page.
|
||||
*/
|
||||
saveState : function(name, value) {
|
||||
if(!window.localStorage) return;
|
||||
|
||||
window.localStorage.setItem('cms-preview-state-' + name, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Load previously stored preferences
|
||||
*/
|
||||
loadState : function(name) {
|
||||
if(!window.localStorage) return;
|
||||
|
||||
return window.localStorage.getItem('cms-preview-state-' + name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable the area - it will not appear in the GUI.
|
||||
* Caveat: the preview will be automatically enabled when ".cms-previewable" class is detected.
|
||||
@ -123,7 +199,7 @@
|
||||
disablePreview: function() {
|
||||
this._loadUrl('about:blank');
|
||||
this._block();
|
||||
this.changeMode('content');
|
||||
this.changeMode('content', false);
|
||||
this.setIsPreviewEnabled(false);
|
||||
return this;
|
||||
},
|
||||
@ -140,7 +216,7 @@
|
||||
// We do not support the split mode in IE < 8.
|
||||
this.changeMode('content');
|
||||
} else {
|
||||
this.changeMode('split');
|
||||
this.changeMode(this.getDefaultMode(), false);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
@ -178,6 +254,7 @@
|
||||
*/
|
||||
_block: function() {
|
||||
this.addClass('blocked');
|
||||
this.find('.cms-preview-overlay').show();
|
||||
return this;
|
||||
},
|
||||
|
||||
@ -186,6 +263,7 @@
|
||||
*/
|
||||
_unblock: function() {
|
||||
this.removeClass('blocked');
|
||||
this.find('.cms-preview-overlay').hide();
|
||||
return this;
|
||||
},
|
||||
|
||||
@ -193,13 +271,25 @@
|
||||
* Update the preview according to browser and CMS section capabilities.
|
||||
*/
|
||||
_initialiseFromContent: function() {
|
||||
var mode, size;
|
||||
|
||||
if (!$('.cms-previewable').length) {
|
||||
this.disablePreview();
|
||||
} else {
|
||||
this.enablePreview();
|
||||
mode = this.loadState('mode');
|
||||
size = this.loadState('size');
|
||||
|
||||
this._moveNavigator();
|
||||
this._loadCurrentState();
|
||||
if(!mode || mode != 'content') {
|
||||
this.enablePreview();
|
||||
this._loadCurrentState();
|
||||
}
|
||||
this.redraw();
|
||||
|
||||
// now check the cookie to see if we have any preview settings that have been
|
||||
// retained for this page from the last visit
|
||||
if(mode) this.changeMode(mode);
|
||||
if(size) this.changeSize(size);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
@ -225,7 +315,7 @@
|
||||
},
|
||||
|
||||
/**
|
||||
* Change the URL of the preview iframe.
|
||||
* Change the URL of the preview iframe (if its not already displayed).
|
||||
*/
|
||||
_loadUrl: function(url) {
|
||||
this.find('iframe').addClass('loading').attr('src', url);
|
||||
@ -240,7 +330,15 @@
|
||||
// Walk through available states and get the URLs.
|
||||
var urlMap = $.map(this.getAllowedStates(), function(name) {
|
||||
var stateLink = $('.cms-preview-states .state-name[data-name=' + name + ']');
|
||||
return stateLink.length ? {name: name, url: stateLink.attr('data-link')} : null;
|
||||
if(stateLink.length) {
|
||||
return {
|
||||
name: name,
|
||||
url: stateLink.attr('data-link'),
|
||||
active: stateLink.is(':radio') ? stateLink.is(':checked') : stateLink.is(':selected')
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
return urlMap;
|
||||
@ -263,7 +361,10 @@
|
||||
// Find current state within currently available states.
|
||||
if (states) {
|
||||
currentState = $.grep(states, function(state, index) {
|
||||
return currentStateName===state.name;
|
||||
return (
|
||||
currentStateName === state.name ||
|
||||
(!currentStateName && state.active)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -361,20 +462,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update the "preview unavailable" overlay according to the class.
|
||||
*/
|
||||
$('.cms-preview.blocked').entwine({
|
||||
onmatch: function() {
|
||||
this.find('.cms-preview-overlay').show();
|
||||
this._super();
|
||||
},
|
||||
onunmatch: function() {
|
||||
this.find('.cms-preview-overlay').hide();
|
||||
this._super();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* "Preview state" functions.
|
||||
* -------------------------------------------------------------------
|
||||
|
@ -286,10 +286,9 @@
|
||||
|
||||
// Copy attributes. We can't replace the node completely
|
||||
// without removing or detaching its children nodes.
|
||||
for(var i=0; i<newNode[0].attributes.length; i++){
|
||||
var attr = newNode[0].attributes[i];
|
||||
node.attr(attr.name, attr.value);
|
||||
}
|
||||
$.each(['id', 'style', 'class', 'data-pagetype'], function(i, attrName) {
|
||||
node.attr(attrName, newNode.attr(attrName));
|
||||
});
|
||||
|
||||
// Replace inner content
|
||||
var origChildren = node.children('ul').detach();
|
||||
|
@ -168,7 +168,8 @@ jQuery.noConflict();
|
||||
* Ensure the user can see the requested section - restore the default view.
|
||||
*/
|
||||
'from .cms-menu-list li a': {
|
||||
onclick: function() {
|
||||
onclick: function(e) {
|
||||
if(e.which > 1) return;
|
||||
this.splitViewMode();
|
||||
}
|
||||
},
|
||||
@ -1050,6 +1051,7 @@ jQuery.noConflict();
|
||||
}(jQuery));
|
||||
|
||||
var statusMessage = function(text, type) {
|
||||
text = $('<div/>').text(text).html(); // Escape HTML entities in text
|
||||
jQuery.noticeAdd({text: text, type: type});
|
||||
};
|
||||
|
||||
|
@ -7,6 +7,6 @@ if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
|
||||
'ModelAdmin.DELETED': "Gelöscht",
|
||||
'ModelAdmin.VALIDATIONERROR': "Validationsfehler",
|
||||
'LeftAndMain.PAGEWASDELETED': "Diese Seite wurde gelöscht.",
|
||||
'LeftAndMain.CONFIRMUNSAVED': "Sind Sie sicher, dasß Sie die Seite verlassen möchten?\n\nWARNUNG: Ihre Änderungen werden nicht gespeichert.\n\nDrücken Sie \"OK\" um fortzufahren, oder \"Abbrechen\" um auf dieser Seite zu bleiben."
|
||||
'LeftAndMain.CONFIRMUNSAVED': "Sind Sie sicher, dass Sie die Seite verlassen möchten?\n\nWARNUNG: Ihre Änderungen werden nicht gespeichert.\n\nDrücken Sie \"OK\" um fortzufahren, oder \"Abbrechen\" um auf dieser Seite zu bleiben."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
14
admin/javascript/lang/nl_NL.js
Normal file
14
admin/javascript/lang/nl_NL.js
Normal file
@ -0,0 +1,14 @@
|
||||
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
|
||||
if(typeof(console) != 'undefined') console.error('Class ss.i18n not defined');
|
||||
} else {
|
||||
ss.i18n.addDictionary('nl_NL', {
|
||||
'LeftAndMain.CONFIRMUNSAVED': "Weet u zeker dat u deze pagina wilt verlaten?\n\WAARSCHUWING: Uw veranderingen zijn niet opgeslagen.\n\nKies OK om te verlaten, of Cancel om op de huidige pagina te blijven.",
|
||||
'LeftAndMain.CONFIRMUNSAVEDSHORT': "WAARSCHUWING: Uw veranderingen zijn niet opgeslagen",
|
||||
'SecurityAdmin.BATCHACTIONSDELETECONFIRM': "Weet u zeker dat u deze groep %s wilt verwijderen?",
|
||||
'ModelAdmin.SAVED': "Opgeslagen",
|
||||
'ModelAdmin.REALLYDELETE': "Weet u zeker dat u wilt verwijderen?",
|
||||
'ModelAdmin.DELETED': "Verwijderd",
|
||||
'ModelAdmin.VALIDATIONERROR': "Validatie fout",
|
||||
'LeftAndMain.PAGEWASDELETED': "Deze pagina is verwijderd. Om een pagina aan te passen, selecteer pagina aan de linkerkant."
|
||||
});
|
||||
}
|
@ -265,7 +265,7 @@ form.small .field, .field.small {
|
||||
}
|
||||
|
||||
input.time {
|
||||
width: ($grid-x * 8); // smaller time field, since input is restricted
|
||||
width: ($grid-x * 11); // smaller time field, since input is restricted
|
||||
}
|
||||
|
||||
/* Hides borders in settings/access. Activated from JS */
|
||||
@ -404,6 +404,13 @@ form.small .field, .field.small {
|
||||
);
|
||||
@include box-shadow(0 0 5px darken($color-button-generic, 20%) inset);
|
||||
}
|
||||
|
||||
&.ss-ui-action-minor {
|
||||
span {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* constructive */
|
||||
&.ss-ui-action-constructive {
|
||||
@ -515,10 +522,6 @@ form.small .field, .field.small {
|
||||
margin-right: 1em;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
&.stacked {
|
||||
|
@ -284,15 +284,12 @@
|
||||
.preview-scroll .preview-device-outer {
|
||||
@include rotate(0deg);
|
||||
@include transition(all 0.3s ease-in 1s);
|
||||
height: 568px;
|
||||
margin: 20px auto 20px;
|
||||
overflow:hidden;
|
||||
padding-top: 16px;
|
||||
width: 335px; // add 15px for approx desktop scrollbar
|
||||
.preview-device-inner {
|
||||
@include rotate(0deg);
|
||||
@include transition(all 0.3s ease-out 1s);
|
||||
width: 335px;
|
||||
}
|
||||
&.rotate {
|
||||
@include rotate(-90deg);
|
||||
@ -312,39 +309,28 @@
|
||||
}
|
||||
&.mobileLandscape .preview-scroll .preview-device-outer {
|
||||
@include transition(all 0.3s ease-out 1s);
|
||||
height: 320px;
|
||||
margin: 12% auto;
|
||||
padding-top: 16px;
|
||||
width: 583px; // add 15px for approx desktop scrollbar
|
||||
.preview-device-inner {
|
||||
@include transition(all 0.3s ease-out 1s);
|
||||
width: 583px;
|
||||
}
|
||||
}
|
||||
&.tablet .preview-scroll .preview-device-outer {
|
||||
@include transition(all 0.3s ease-out 1s);
|
||||
height: 1024px;
|
||||
margin: 0 auto;
|
||||
width: 783px; // add 15px for approx desktop scrollbar
|
||||
.preview-device-inner {
|
||||
@include transition(all 0.3s ease-out 1s);
|
||||
width: 783px;
|
||||
}
|
||||
}
|
||||
&.tabletLandscape .preview-scroll .preview-device-outer {
|
||||
@include transition(all 0.3s ease-out 1s);
|
||||
height: 768px;
|
||||
margin: 0 auto;
|
||||
width: 1039px;// add 15px for approx desktop scrollbar
|
||||
.preview-device-inner {
|
||||
@include transition(all 0.3s ease-out 1s);
|
||||
width: 1039px;
|
||||
}
|
||||
}
|
||||
&.desktop .preview-scroll .preview-device-outer {
|
||||
@include transition(all 0.3s ease-out 1s);
|
||||
height: 800px;
|
||||
margin: 0 auto;
|
||||
width: 1024px;
|
||||
}
|
||||
}
|
@ -582,6 +582,10 @@ body.cms {
|
||||
|
||||
.description {
|
||||
font-style: italic;
|
||||
// Undo some generic styles from tooltips
|
||||
display: inline;
|
||||
clear: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -823,6 +827,7 @@ body.cms {
|
||||
display: block;
|
||||
|
||||
.view-mode-batchactions-wrapper {
|
||||
height: 18px;
|
||||
float: left;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid #aaa;
|
||||
@ -830,9 +835,14 @@ body.cms {
|
||||
background-color: #D9D9D9;
|
||||
@include background-image(linear-gradient(top, #fff, #D9D9D9));
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
|
||||
input {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
label {
|
||||
vertical-align: middle;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -842,18 +852,18 @@ body.cms {
|
||||
}
|
||||
|
||||
&.inactive .view-mode-batchactions-wrapper {
|
||||
border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
|
||||
label {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
form > * {
|
||||
display: block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
|
||||
form.cms-batch-actions {
|
||||
float: left;
|
||||
}
|
||||
@ -862,13 +872,24 @@ body.cms {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chzn-container-single .chzn-single {
|
||||
margin-left: -1px;
|
||||
border-radius: 0;
|
||||
@include background-image(linear-gradient(top, #fff, #D9D9D9));
|
||||
.chzn-container-single {
|
||||
display: block;
|
||||
|
||||
.chzn-single {
|
||||
margin-left: -1px;
|
||||
border-radius: 0;
|
||||
@include background-image(linear-gradient(top, #fff, #D9D9D9));
|
||||
|
||||
span {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cms-batch-actions .Actions .ss-ui-button {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
height: 28px;
|
||||
margin-left: -1px;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
@ -303,3 +303,21 @@ table.ss-gridfield-table {
|
||||
width: 190px; /* Width 100% not calculating by ie7 */
|
||||
}
|
||||
}
|
||||
|
||||
/* Insert Media Area */
|
||||
|
||||
.ui-dialog-titlebar {
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
.ss-uploadfield-item-info {
|
||||
.dimensions {
|
||||
input {
|
||||
float:left;
|
||||
width:150px;
|
||||
}
|
||||
.fieldgroup-field.last {
|
||||
margin-left:16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@
|
||||
<a href="$Link" <% if Code == 'Help' %>target="_blank"<% end_if%>>
|
||||
<span class="icon icon-16 icon-{$Code.LowerCase}"> </span>
|
||||
<span class="text">$Title</span>
|
||||
</a>
|
||||
</a>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
|
@ -24,8 +24,8 @@
|
||||
<fieldset id="preview-states" class="cms-preview-states switch-states size_{$Items.Count}">
|
||||
<div class="switch">
|
||||
<% loop Items %>
|
||||
<input id="$Title" data-name="$Name" class="state-name $FirstLast" data-link="$Link" name="view" type="radio" <% if First %>checked<% end_if %>>
|
||||
<label for="$Title"<% if First %> class="active"<% end_if %>><span>$Title</span></label>
|
||||
<input id="$Title" data-name="$Name" class="state-name $FirstLast" data-link="$Link" name="view" type="radio" <% if isActive %>checked<% end_if %>>
|
||||
<label for="$Title"<% if isActive %> class="active"<% end_if %>><span>$Title</span></label>
|
||||
<% end_loop %>
|
||||
<span class="slide-button"></span>
|
||||
</div>
|
||||
@ -34,7 +34,7 @@
|
||||
<span id="preview-state-dropdown" class="cms-preview-states field dropdown">
|
||||
<select title="<% _t('SilverStripeNavigator.PreviewState', 'Preview State') %>" id="preview-states" class="preview-state dropdown nolabel" autocomplete="off" name="preview-state">
|
||||
<% loop Items %>
|
||||
<option name="$Name" data-name="$Name" data-link="$Link" class="state-name $FirstLast" value="$Link" >
|
||||
<option name="$Name" data-name="$Name" data-link="$Link" class="state-name $FirstLast" value="$Link" <% if isActive %>selected<% end_if %>>
|
||||
$Title
|
||||
</option>
|
||||
<% end_loop %>
|
||||
|
@ -190,17 +190,19 @@ class RSSFeed extends ViewableData {
|
||||
SSViewer::set_source_file_comments(false);
|
||||
$response = Controller::curr()->getResponse();
|
||||
|
||||
$response = Controller::curr()->getResponse();
|
||||
|
||||
if(is_int($this->lastModified)) {
|
||||
HTTP::register_modification_timestamp($this->lastModified);
|
||||
$response->addHeader('Last-Modified', gmdate("D, d M Y H:i:s", $this->lastModified) . ' GMT');
|
||||
$response->addHeader("Last-Modified", gmdate("D, d M Y H:i:s", $this->lastModified) . ' GMT');
|
||||
}
|
||||
if(!empty($this->etag)) {
|
||||
HTTP::register_etag($this->etag);
|
||||
}
|
||||
|
||||
if(!headers_sent()) {
|
||||
HTTP::add_cache_headers($response);
|
||||
$response->addHeader('Content-Type', 'text/xml');
|
||||
HTTP::add_cache_headers();
|
||||
$response->addHeader("Content-Type", "application/rss+xml");
|
||||
}
|
||||
|
||||
SSViewer::set_source_file_comments($prevState);
|
||||
@ -294,7 +296,7 @@ class RSSFeed_Entry extends ViewableData {
|
||||
* @return string Returns the description of the entry.
|
||||
*/
|
||||
public function Description() {
|
||||
return $this->rssField($this->descriptionField, 'Text');
|
||||
return $this->rssField($this->descriptionField, 'HTMLText');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,6 +17,27 @@ class RestfulService extends ViewableData {
|
||||
protected $customHeaders = array();
|
||||
protected $proxy;
|
||||
protected static $default_proxy;
|
||||
protected static $default_curl_options = array();
|
||||
|
||||
/**
|
||||
* set a curl option that will be applied to all requests as default
|
||||
* {@see http://php.net/manual/en/function.curl-setopt.php#refsect1-function.curl-setopt-parameters}
|
||||
*
|
||||
* @param int $option The cURL opt Constant
|
||||
* @param mixed $value The cURL opt value
|
||||
*/
|
||||
public static function set_default_curl_option($option, $value) {
|
||||
self::$default_curl_options[$option] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* set many defauly curl options at once
|
||||
*/
|
||||
public static function set_default_curl_options($optionArray) {
|
||||
foreach ($optionArray as $option => $value) {
|
||||
self::set_default_curl_option($option, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default proxy settings for outbound RestfulService connections
|
||||
@ -114,12 +135,20 @@ class RestfulService extends ViewableData {
|
||||
|
||||
assert(in_array($method, array('GET','POST','PUT','DELETE','HEAD','OPTIONS')));
|
||||
|
||||
$cachedir = TEMP_FOLDER; // Default silverstripe cache
|
||||
$cache_file = md5($url); // Encoded name of cache file
|
||||
$cache_path = $cachedir."/xmlresponse_$cache_file";
|
||||
$cache_path = $this->getCachePath(array(
|
||||
$url,
|
||||
$method,
|
||||
$data,
|
||||
array_merge((array)$this->customHeaders, (array)$headers),
|
||||
array_merge(self::$default_curl_options,$curlOptions),
|
||||
$this->getBasicAuthString()
|
||||
));
|
||||
|
||||
// Check for unexpired cached feed (unless flush is set)
|
||||
if(!isset($_GET['flush']) && @file_exists($cache_path)
|
||||
//assume any cache_expire that is 0 or less means that we dont want to
|
||||
// cache
|
||||
if($this->cache_expire > 0 && !isset($_GET['flush'])
|
||||
&& @file_exists($cache_path)
|
||||
&& @filemtime($cache_path) + $this->cache_expire > time()) {
|
||||
|
||||
$store = file_get_contents($cache_path);
|
||||
@ -140,10 +169,10 @@ class RestfulService extends ViewableData {
|
||||
$store = file_get_contents($cache_path);
|
||||
$cachedResponse = unserialize($store);
|
||||
|
||||
$response->setCachedBody($cachedResponse->getBody());
|
||||
$response->setCachedResponse($cachedResponse);
|
||||
}
|
||||
else {
|
||||
$response->setCachedBody(false);
|
||||
$response->setCachedResponse(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -167,6 +196,7 @@ class RestfulService extends ViewableData {
|
||||
$timeout = 5;
|
||||
$sapphireInfo = new SapphireInfo();
|
||||
$useragent = 'SilverStripe/' . $sapphireInfo->Version();
|
||||
$curlOptions = array_merge(self::$default_curl_options, $curlOptions);
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
@ -174,6 +204,8 @@ class RestfulService extends ViewableData {
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
|
||||
if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
//include headers in the response
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
|
||||
// Add headers
|
||||
if($this->customHeaders) {
|
||||
@ -183,7 +215,7 @@ class RestfulService extends ViewableData {
|
||||
if($headers) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
// Add authentication
|
||||
if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, "$this->authUsername:$this->authPassword");
|
||||
if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, $this->getBasicAuthString());
|
||||
|
||||
// Add fields to POST and PUT requests
|
||||
if($method == 'POST') {
|
||||
@ -208,26 +240,112 @@ class RestfulService extends ViewableData {
|
||||
curl_setopt_array($ch, $curlOptions);
|
||||
|
||||
// Run request
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
$responseBody = curl_exec($ch);
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
// Problem verifying the server SSL certificate; just ignore it as it's not mandatory
|
||||
if(strpos($curlError,'14090086') !== false) {
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
$responseBody = curl_exec($ch);
|
||||
$curlError = curl_error($ch);
|
||||
}
|
||||
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if($curlError !== '' || $statusCode == 0) $statusCode = 500;
|
||||
|
||||
$response = new RestfulService_Response($responseBody, $statusCode);
|
||||
$rawResponse = curl_exec($ch);
|
||||
$response = $this->extractResponse($ch, $rawResponse);
|
||||
curl_close($ch);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to return the auth string. This helps consistency through the
|
||||
* class but also allows tests to pull it out when generating the expected
|
||||
* cache keys
|
||||
*
|
||||
* @see {self::getCachePath()}
|
||||
* @see {RestfulServiceTest::createFakeCachedResponse()}
|
||||
*
|
||||
* @return string The auth string to be base64 encoded
|
||||
*/
|
||||
protected function getBasicAuthString() {
|
||||
return $this->authUsername . ':' . $this->authPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cache key based on any cache data sent. The cache data can be
|
||||
* any type
|
||||
*
|
||||
* @param mixed $cacheData The cache seed for generating the key
|
||||
* @param string the md5 encoded cache seed.
|
||||
*/
|
||||
protected function generateCacheKey($cacheData) {
|
||||
return md5(var_export($cacheData, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the cache path
|
||||
*
|
||||
* This is mainly so that the cache path can be generated in a consistent
|
||||
* way in tests without having to hard code the cachekey generate function
|
||||
* in tests
|
||||
*
|
||||
* @param mixed $cacheData The cache seed {@see self::generateCacheKey}
|
||||
*
|
||||
* @return string The path to the cache file
|
||||
*/
|
||||
protected function getCachePath($cacheData) {
|
||||
return TEMP_FOLDER . "/xmlresponse_" . $this->generateCacheKey($cacheData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the response body and headers from a full curl response
|
||||
*
|
||||
* @param curl_handle $ch The curl handle for the request
|
||||
* @param string $rawResponse The raw response text
|
||||
*
|
||||
* @return RestfulService_Response The response object
|
||||
*/
|
||||
protected function extractResponse($ch, $rawResponse) {
|
||||
//get the status code
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
//normalise the status code
|
||||
if($curlError !== '' || $statusCode == 0) $statusCode = 500;
|
||||
//calculate the length of the header and extract it
|
||||
$headerLength = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
$rawHeaders = substr($rawResponse, 0, $headerLength);
|
||||
//extract the body
|
||||
$body = substr($rawResponse, $headerLength);
|
||||
//parse the headers
|
||||
$headers = $this->parseRawHeaders($rawHeaders);
|
||||
//return the response object
|
||||
return new RestfulService_Response($body, $statusCode, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes raw headers and parses them to turn them to an associative array
|
||||
*
|
||||
* Any header that we see more than once is turned into an array.
|
||||
*
|
||||
* This is meant to mimic http_parse_headers {@link http://php.net/manual/en/function.http-parse-headers.php}
|
||||
* thanks to comment #77241 on that page for foundation of this
|
||||
*
|
||||
* @param string $rawHeaders The raw header string
|
||||
* @return array The assosiative array of headers
|
||||
*/
|
||||
protected function parseRawHeaders($rawHeaders) {
|
||||
$headers = array();
|
||||
$fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $rawHeaders));
|
||||
foreach( $fields as $field ) {
|
||||
if( preg_match('/([^:]+): (.+)/m', $field, $match) ) {
|
||||
$match[1] = preg_replace_callback(
|
||||
'/(?<=^|[\x09\x20\x2D])./',
|
||||
create_function('$matches', 'return strtoupper($matches[0]);'),
|
||||
strtolower(trim($match[1]))
|
||||
);
|
||||
if( isset($headers[$match[1]]) ) {
|
||||
if (!is_array($headers[$match[1]])) {
|
||||
$headers[$match[1]] = array($headers[$match[1]]);
|
||||
}
|
||||
$headers[$match[1]][] = $match[2];
|
||||
} else {
|
||||
$headers[$match[1]] = trim($match[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a full request url
|
||||
* @param string
|
||||
@ -418,10 +536,10 @@ class RestfulService_Response extends SS_HTTPResponse {
|
||||
protected $simpleXML;
|
||||
|
||||
/**
|
||||
* @var boolean It should be populated with cached content
|
||||
* @var boolean It should be populated with cached request
|
||||
* when a request referring to this response was unsuccessful
|
||||
*/
|
||||
protected $cachedBody = false;
|
||||
protected $cachedResponse = false;
|
||||
|
||||
public function __construct($body, $statusCode = 200, $headers = null) {
|
||||
$this->setbody($body);
|
||||
@ -441,18 +559,44 @@ class RestfulService_Response extends SS_HTTPResponse {
|
||||
return $this->simpleXML;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the cached response object. This allows you to access the cached
|
||||
* eaders, not just the cached body.
|
||||
*
|
||||
* @return RestfulSerivice_Response The cached response object
|
||||
*/
|
||||
public function getCachedResponse() {
|
||||
return $this->cachedResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCachedBody() {
|
||||
return $this->cachedBody;
|
||||
if ($this->cachedResponse) {
|
||||
return $this->cachedResponse->getBody();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
public function setCachedBody($content) {
|
||||
Deprecation::notice('3.1', 'Setting the response body is now deprecated, set the cached request instead');
|
||||
if (!$this->cachedResponse) {
|
||||
$this->cachedResponse = new RestfulService_Response($content);
|
||||
}
|
||||
else {
|
||||
$this->cachedResponse->setBody = $content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
public function setCachedBody($content) {
|
||||
$this->cachedBody = $content;
|
||||
public function setCachedResponse($response) {
|
||||
$this->cachedResponse = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -467,8 +611,8 @@ class RestfulService_Response extends SS_HTTPResponse {
|
||||
*/
|
||||
public function xpath_one($xpath) {
|
||||
$items = $this->xpath($xpath);
|
||||
return $items[0];
|
||||
if (isset($items[0])) {
|
||||
return $items[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -41,6 +41,7 @@
|
||||
*
|
||||
* Email:
|
||||
* - SS_SEND_ALL_EMAILS_TO: If you set this define, all emails will be redirected to this address.
|
||||
* - SS_SEND_ALL_EMAILS_FROM: If you set this define, all emails will be send from this address.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage core
|
||||
@ -105,7 +106,10 @@ if(defined('SS_DATABASE_USERNAME') && defined('SS_DATABASE_PASSWORD')) {
|
||||
}
|
||||
|
||||
if(defined('SS_SEND_ALL_EMAILS_TO')) {
|
||||
Email::send_all_emails_to(SS_SEND_ALL_EMAILS_TO);
|
||||
Config::inst()->update("Email","send_all_emails_to", SS_SEND_ALL_EMAILS_TO);
|
||||
}
|
||||
if(defined('SS_SEND_ALL_EMAILS_FROM')) {
|
||||
Config::inst()->update("Email","send_all_emails_from", SS_SEND_ALL_EMAILS_FROM);
|
||||
}
|
||||
|
||||
if(defined('SS_DEFAULT_ADMIN_USERNAME')) {
|
||||
|
@ -182,36 +182,25 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
|
||||
* Controller's default action handler. It will call the method named in $Action, if that method exists.
|
||||
* If $Action isn't given, it will use "index" as a default.
|
||||
*/
|
||||
public function handleAction($request) {
|
||||
// urlParams, requestParams, and action are set for backward compatability
|
||||
public function handleAction($request, $action) {
|
||||
foreach($request->latestParams() as $k => $v) {
|
||||
if($v || !isset($this->urlParams[$k])) $this->urlParams[$k] = $v;
|
||||
}
|
||||
|
||||
$this->action = str_replace("-","_",$request->param('Action'));
|
||||
$this->action = $action;
|
||||
$this->requestParams = $request->requestVars();
|
||||
if(!$this->action) $this->action = 'index';
|
||||
|
||||
if(!$this->hasAction($this->action)) {
|
||||
$this->httpError(404, "The action '$this->action' does not exist in class $this->class");
|
||||
}
|
||||
|
||||
// run & init are manually disabled, because they create infinite loops and other dodgy situations
|
||||
if(!$this->checkAccessAction($this->action) || in_array(strtolower($this->action), array('run', 'init'))) {
|
||||
return $this->httpError(403, "Action '$this->action' isn't allowed on class $this->class");
|
||||
}
|
||||
|
||||
if($this->hasMethod($this->action)) {
|
||||
$result = $this->{$this->action}($request);
|
||||
|
||||
|
||||
if($this->hasMethod($action)) {
|
||||
$result = parent::handleAction($request, $action);
|
||||
|
||||
// If the action returns an array, customise with it before rendering the template.
|
||||
if(is_array($result)) {
|
||||
return $this->getViewer($this->action)->process($this->customise($result));
|
||||
return $this->getViewer($action)->process($this->customise($result));
|
||||
} else {
|
||||
return $result;
|
||||
}
|
||||
} else {
|
||||
return $this->getViewer($this->action)->process($this);
|
||||
return $this->getViewer($action)->process($this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -454,7 +443,7 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
|
||||
public function redirect($url, $code=302) {
|
||||
if(!$this->response) $this->response = new SS_HTTPResponse();
|
||||
|
||||
if($this->response->getHeader('Location')) {
|
||||
if($this->response->getHeader('Location') && $this->response->getHeader('Location') != $url) {
|
||||
user_error("Already directed to " . $this->response->getHeader('Location')
|
||||
. "; now trying to direct to $url", E_USER_WARNING);
|
||||
return;
|
||||
|
@ -141,18 +141,7 @@ class Director implements TemplateGlobalProvider {
|
||||
|
||||
$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
|
||||
if ($res !== false) {
|
||||
// Set content length (according to RFC2616)
|
||||
if(
|
||||
!headers_sent()
|
||||
&& $response->getBody()
|
||||
&& $req->httpMethod() != 'HEAD'
|
||||
&& $response->getStatusCode() >= 200
|
||||
&& !in_array($response->getStatusCode(), array(204, 304))
|
||||
) {
|
||||
$response->fixContentLength();
|
||||
}
|
||||
|
||||
$response->output();
|
||||
$response->output();
|
||||
} else {
|
||||
// @TODO Proper response here.
|
||||
throw new SS_HTTPResponse_Exception("Invalid response");
|
||||
@ -674,6 +663,9 @@ class Director implements TemplateGlobalProvider {
|
||||
$matched = false;
|
||||
|
||||
if($patterns) {
|
||||
// Calling from the command-line?
|
||||
if(!isset($_SERVER['REQUEST_URI'])) return;
|
||||
|
||||
// protect portions of the site based on the pattern
|
||||
$relativeURL = self::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI']));
|
||||
foreach($patterns as $pattern) {
|
||||
|
@ -41,20 +41,43 @@ class HTTP {
|
||||
*/
|
||||
public static function absoluteURLs($html) {
|
||||
$html = str_replace('$CurrentPageURL', $_SERVER['REQUEST_URI'], $html);
|
||||
return HTTP::urlRewriter($html, '(substr($URL,0,1) == "/") ? ( Director::protocolAndHost() . $URL ) :'
|
||||
. ' ( (preg_match("/^[A-Za-z]+:/", $URL)) ? $URL : Director::absoluteBaseURL() . $URL )' );
|
||||
return HTTP::urlRewriter($html, function($url) {
|
||||
return Director::absoluteURL($url, true);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Rewrite all the URLs in the given content, evaluating the given string as PHP code
|
||||
/**
|
||||
* Rewrite all the URLs in the given content, evaluating the given string as PHP code.
|
||||
*
|
||||
* Put $URL where you want the URL to appear, however, you can't embed $URL in strings
|
||||
* Some example code:
|
||||
* '"../../" . $URL'
|
||||
* 'myRewriter($URL)'
|
||||
* '(substr($URL,0,1)=="/") ? "../" . substr($URL,1) : $URL'
|
||||
* <ul>
|
||||
* <li><code>'"../../" . $URL'</code></li>
|
||||
* <li><code>'myRewriter($URL)'</code></li>
|
||||
* <li><code>'(substr($URL,0,1)=="/") ? "../" . substr($URL,1) : $URL'</code></li>
|
||||
* </ul>
|
||||
*
|
||||
* As of 3.2 $code should be a callable which takes a single parameter and returns
|
||||
* the rewritten URL. e.g.
|
||||
*
|
||||
* <code>
|
||||
* function($url) {
|
||||
* return Director::absoluteURL($url, true);
|
||||
* }
|
||||
* </code>
|
||||
*
|
||||
* @param string $content The HTML to search for links to rewrite
|
||||
* @param string|callable $code Either a string that can evaluate to an expression
|
||||
* to rewrite links (depreciated), or a callable that takes a single
|
||||
* parameter and returns the rewritten URL
|
||||
* @return The content with all links rewritten as per the logic specified in $code
|
||||
*/
|
||||
public static function urlRewriter($content, $code) {
|
||||
if(!is_callable($code)) {
|
||||
Deprecation::notice(3.1, 'HTTP::urlRewriter expects a callable as the second parameter');
|
||||
}
|
||||
|
||||
// Replace attributes
|
||||
$attribs = array("src","background","a" => "href","link" => "href", "base" => "href");
|
||||
foreach($attribs as $tag => $attrib) {
|
||||
if(!is_numeric($tag)) $tagPrefix = "$tag ";
|
||||
@ -64,19 +87,28 @@ class HTTP {
|
||||
$regExps[] = "/(<{$tagPrefix}[^>]*$attrib *= *')([^']*)(')/i";
|
||||
$regExps[] = "/(<{$tagPrefix}[^>]*$attrib *= *)([^\"' ]*)( )/i";
|
||||
}
|
||||
$regExps[] = '/(background-image:[^;]*url *\()([^)]+)(\))/i';
|
||||
$regExps[] = '/(background:[^;]*url *\()([^)]+)(\))/i';
|
||||
$regExps[] = '/(list-style-image:[^;]*url *\()([^)]+)(\))/i';
|
||||
$regExps[] = '/(list-style:[^;]*url *\()([^)]+)(\))/i';
|
||||
// Replace css styles
|
||||
// @todo - http://www.css3.info/preview/multiple-backgrounds/
|
||||
$styles = array('background-image', 'background', 'list-style-image', 'list-style', 'content');
|
||||
foreach($styles as $style) {
|
||||
$regExps[] = "/($style:[^;]*url *\(\")([^\"]+)(\"\))/i";
|
||||
$regExps[] = "/($style:[^;]*url *\(')([^']+)('\))/i";
|
||||
$regExps[] = "/($style:[^;]*url *\()([^\"\)')]+)(\))/i";
|
||||
}
|
||||
|
||||
// Make
|
||||
// Callback for regexp replacement
|
||||
$callback = function($matches) use($code) {
|
||||
return
|
||||
stripslashes($matches[1]) .
|
||||
str_replace('$URL', stripslashes($matches[2]), $code) .
|
||||
stripslashes($matches[3]);
|
||||
if(is_callable($code)) {
|
||||
$rewritten = $code($matches[2]);
|
||||
} else {
|
||||
// Expose the $URL variable to be used by the $code expression
|
||||
$URL = $matches[2];
|
||||
$rewritten = eval("return ($code);");
|
||||
}
|
||||
return $matches[1] . $rewritten . $matches[3];
|
||||
};
|
||||
|
||||
// Execute each expression
|
||||
foreach($regExps as $regExp) {
|
||||
$content = preg_replace_callback($regExp, $callback, $content);
|
||||
}
|
||||
@ -282,7 +314,7 @@ class HTTP {
|
||||
$responseHeaders["Cache-Control"] = "max-age=" . self::$cache_age . ", must-revalidate, no-transform";
|
||||
$responseHeaders["Pragma"] = "";
|
||||
|
||||
// To do: User-Agent should only be added in situations where you *are* actually varying according to user-agent.
|
||||
// To do: User-Agent should only be added in situations where you *are* actually varying according to it.
|
||||
$responseHeaders['Vary'] = 'Cookie, X-Forwarded-Protocol, User-Agent, Accept';
|
||||
|
||||
} else {
|
||||
@ -293,11 +325,12 @@ class HTTP {
|
||||
$responseHeaders["Last-Modified"] = self::gmt_date(self::$modification_date);
|
||||
|
||||
/* Chrome ignores Varies when redirecting back (http://code.google.com/p/chromium/issues/detail?id=79758)
|
||||
which means that if you log out, you get redirected back to a page which Chrome then checks against last-modified (which passes, getting a 304)
|
||||
when it shouldn't be trying to use that page at all because it's the "logged in" version.
|
||||
which means that if you log out, you get redirected back to a page which Chrome then checks against
|
||||
last-modified (which passes, getting a 304) when it shouldn't be trying to use that page at all because
|
||||
it's the "logged in" version.
|
||||
|
||||
By also using and etag that includes both the modification date and all the varies values which we also check against we can catch
|
||||
this and not return a 304
|
||||
By also using and etag that includes both the modification date and all the varies values which we also
|
||||
check against we can catch this and not return a 304
|
||||
*/
|
||||
$etagParts = array(self::$modification_date, serialize($_COOKIE));
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])) $etagParts[] = $_SERVER['HTTP_X_FORWARDED_PROTOCOL'];
|
||||
|
@ -610,7 +610,7 @@ class SS_HTTPRequest implements ArrayAccess {
|
||||
for($i=0;$i<$count;$i++) {
|
||||
$value = array_shift($this->dirParts);
|
||||
|
||||
if(!$value) break;
|
||||
if($value === null) break;
|
||||
|
||||
$return[] = $value;
|
||||
}
|
||||
|
@ -160,6 +160,9 @@ class SS_HTTPResponse {
|
||||
*/
|
||||
public function setBody($body) {
|
||||
$this->body = $body;
|
||||
|
||||
// Set content-length in bytes. Use mbstring to avoid problems with mb_internal_encoding() and mbstring.func_overload
|
||||
$this->headers['Content-Length'] = mb_strlen($this->body,'8bit');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -241,11 +244,17 @@ class SS_HTTPResponse {
|
||||
<meta http-equiv=\"refresh\" content=\"1; url=$url\" />
|
||||
<script type=\"text/javascript\">setTimeout('window.location.href = \"$url\"', 50);</script>";
|
||||
} else {
|
||||
if(!headers_sent()) {
|
||||
$line = $file = null;
|
||||
if(!headers_sent($file, $line)) {
|
||||
header($_SERVER['SERVER_PROTOCOL'] . " $this->statusCode " . $this->getStatusDescription());
|
||||
foreach($this->headers as $header => $value) {
|
||||
header("$header: $value", true, $this->statusCode);
|
||||
}
|
||||
} else {
|
||||
// It's critical that these status codes are sent; we need to report a failure if not.
|
||||
if($this->statusCode >= 300) {
|
||||
user_error("Couldn't set response type to $this->statusCode because of output on line $line of $file", E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
// Only show error pages or generic "friendly" errors if the status code signifies
|
||||
@ -269,14 +278,6 @@ class SS_HTTPResponse {
|
||||
public function isFinished() {
|
||||
return in_array($this->statusCode, array(301, 302, 401, 403));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content-length in bytes. Should be called right before {@link output()}.
|
||||
*/
|
||||
public function fixContentLength() {
|
||||
// Use mbstring to avoid problems with mb_internal_encoding() and mbstring.func_overload
|
||||
$this->headers['Content-Length'] = mb_strlen($this->body,'8bit');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@ class RequestHandler extends ViewableData {
|
||||
* </code>
|
||||
*
|
||||
* Form getters count as URL actions as well, and should be included in allowed_actions.
|
||||
* Form actions on the other handed (first argument to {@link FormAction()} shoudl NOT be included,
|
||||
* Form actions on the other handed (first argument to {@link FormAction()} should NOT be included,
|
||||
* these are handled separately through {@link Form->httpSubmission}. You can control access on form actions
|
||||
* either by conditionally removing {@link FormAction} in the form construction,
|
||||
* or by defining $allowed_actions in your {@link Form} class.
|
||||
@ -145,100 +145,175 @@ class RequestHandler extends ViewableData {
|
||||
|
||||
$this->request = $request;
|
||||
$this->setDataModel($model);
|
||||
|
||||
$match = $this->findAction($request);
|
||||
|
||||
// If nothing matches, return this object
|
||||
if (!$match) return $this;
|
||||
|
||||
// Start to find what action to call. Start by using what findAction returned
|
||||
$action = $match['action'];
|
||||
|
||||
// We used to put "handleAction" as the action on controllers, but (a) this could only be called when
|
||||
// you had $Action in your rule, and (b) RequestHandler didn't have one. $Action is better
|
||||
if ($action == 'handleAction') {
|
||||
Deprecation::notice('3.2.0', 'Calling handleAction directly is deprecated - use $Action instead');
|
||||
$action = '$Action';
|
||||
}
|
||||
|
||||
// Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
|
||||
if($action[0] == '$') {
|
||||
$action = str_replace("-", "_", $request->latestParam(substr($action,1)));
|
||||
}
|
||||
|
||||
if(!$action) {
|
||||
if(isset($_REQUEST['debug_request'])) {
|
||||
Debug::message("Action not set; using default action method name 'index'");
|
||||
}
|
||||
$action = "index";
|
||||
} else if(!is_string($action)) {
|
||||
user_error("Non-string method name: " . var_export($action, true), E_USER_ERROR);
|
||||
}
|
||||
|
||||
$className = get_class($this);
|
||||
|
||||
if(!$this->hasAction($action)) {
|
||||
return new SS_HTTPResponse("Action '$action' isn't available on class $className.", 404);
|
||||
}
|
||||
|
||||
if(!$this->checkAccessAction($action) || in_array(strtolower($action), array('run', 'init'))) {
|
||||
return new SS_HTTPResponse("Action '$action' isn't allowed on class $className.", 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->handleAction($request, $action);
|
||||
}
|
||||
catch (SS_HTTPResponse_Exception $e) {
|
||||
return $e->getResponse();
|
||||
}
|
||||
catch(PermissionFailureException $e) {
|
||||
$result = Security::permissionFailure(null, $e->getMessage());
|
||||
}
|
||||
|
||||
if($result instanceof SS_HTTPResponse && $result->isError()) {
|
||||
if(isset($_REQUEST['debug_request'])) Debug::message("Rule resulted in HTTP error; breaking");
|
||||
return $result;
|
||||
}
|
||||
|
||||
// If we return a RequestHandler, call handleRequest() on that, even if there is no more URL to
|
||||
// parse. It might have its own handler. However, we only do this if we haven't just parsed an
|
||||
// empty rule ourselves, to prevent infinite loops. Also prevent further handling of controller
|
||||
// actions which return themselves to avoid infinite loops.
|
||||
$matchedRuleWasEmpty = $request->isEmptyPattern($match['rule']);
|
||||
$resultIsRequestHandler = is_object($result) && $result instanceof RequestHandler;
|
||||
|
||||
if($this !== $result && !$matchedRuleWasEmpty && $resultIsRequestHandler) {
|
||||
$returnValue = $result->handleRequest($request, $model);
|
||||
|
||||
// Array results can be used to handle
|
||||
if(is_array($returnValue)) $returnValue = $this->customise($returnValue);
|
||||
|
||||
return $returnValue;
|
||||
|
||||
// If we return some other data, and all the URL is parsed, then return that
|
||||
} else if($request->allParsed()) {
|
||||
return $result;
|
||||
|
||||
// But if we have more content on the URL and we don't know what to do with it, return an error.
|
||||
} else {
|
||||
return $this->httpError(404, "I can't handle sub-URLs of a $this->class object.");
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function findAction($request) {
|
||||
$handlerClass = ($this->class) ? $this->class : get_class($this);
|
||||
|
||||
// We stop after RequestHandler; in other words, at ViewableData
|
||||
while($handlerClass && $handlerClass != 'ViewableData') {
|
||||
$urlHandlers = Config::inst()->get($handlerClass, 'url_handlers', Config::FIRST_SET);
|
||||
$urlHandlers = Config::inst()->get($handlerClass, 'url_handlers', Config::UNINHERITED);
|
||||
|
||||
if($urlHandlers) foreach($urlHandlers as $rule => $action) {
|
||||
if(isset($_REQUEST['debug_request'])) {
|
||||
Debug::message("Testing '$rule' with '" . $request->remaining() . "' on $this->class");
|
||||
}
|
||||
if($params = $request->match($rule, true)) {
|
||||
// Backwards compatible setting of url parameters, please use SS_HTTPRequest->latestParam() instead
|
||||
//Director::setUrlParams($request->latestParams());
|
||||
|
||||
|
||||
if($request->match($rule, true)) {
|
||||
if(isset($_REQUEST['debug_request'])) {
|
||||
Debug::message("Rule '$rule' matched to action '$action' on $this->class."
|
||||
. " Latest request params: " . var_export($request->latestParams(), true));
|
||||
Debug::message(
|
||||
"Rule '$rule' matched to action '$action' on $this->class. ".
|
||||
"Latest request params: " . var_export($request->latestParams(), true)
|
||||
);
|
||||
}
|
||||
|
||||
// Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
|
||||
if($action[0] == '$') $action = $params[substr($action,1)];
|
||||
|
||||
if($this->checkAccessAction($action)) {
|
||||
if(!$action) {
|
||||
if(isset($_REQUEST['debug_request'])) {
|
||||
Debug::message("Action not set; using default action method name 'index'");
|
||||
}
|
||||
$action = "index";
|
||||
} else if(!is_string($action)) {
|
||||
user_error("Non-string method name: " . var_export($action, true), E_USER_ERROR);
|
||||
}
|
||||
|
||||
try {
|
||||
if(!$this->hasMethod($action)) {
|
||||
return $this->httpError(404, "Action '$action' isn't available on class "
|
||||
. get_class($this) . ".");
|
||||
}
|
||||
$result = $this->$action($request);
|
||||
} catch(SS_HTTPResponse_Exception $responseException) {
|
||||
$result = $responseException->getResponse();
|
||||
}
|
||||
} else {
|
||||
return $this->httpError(403, "Action '$action' isn't allowed on class " . get_class($this));
|
||||
}
|
||||
|
||||
if($result instanceof SS_HTTPResponse && $result->isError()) {
|
||||
if(isset($_REQUEST['debug_request'])) Debug::message("Rule resulted in HTTP error; breaking");
|
||||
return $result;
|
||||
}
|
||||
|
||||
// If we return a RequestHandler, call handleRequest() on that, even if there is no more URL to
|
||||
// parse. It might have its own handler. However, we only do this if we haven't just parsed an
|
||||
// empty rule ourselves, to prevent infinite loops. Also prevent further handling of controller
|
||||
// actions which return themselves to avoid infinite loops.
|
||||
if($this !== $result && !$request->isEmptyPattern($rule) && is_object($result)
|
||||
&& $result instanceof RequestHandler) {
|
||||
|
||||
$returnValue = $result->handleRequest($request, $model);
|
||||
|
||||
// Array results can be used to handle
|
||||
if(is_array($returnValue)) $returnValue = $this->customise($returnValue);
|
||||
|
||||
return $returnValue;
|
||||
|
||||
// If we return some other data, and all the URL is parsed, then return that
|
||||
} else if($request->allParsed()) {
|
||||
return $result;
|
||||
|
||||
// But if we have more content on the URL and we don't know what to do with it, return an error.
|
||||
} else {
|
||||
return $this->httpError(404, "I can't handle sub-URLs of a $this->class object.");
|
||||
}
|
||||
|
||||
return $this;
|
||||
return array('rule' => $rule, 'action' => $action);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$handlerClass = get_parent_class($handlerClass);
|
||||
}
|
||||
|
||||
// If nothing matches, return this object
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request, and an action name, call that action name on this RequestHandler
|
||||
*
|
||||
* Must not raise SS_HTTPResponse_Exceptions - instead it should return
|
||||
*
|
||||
* @param $request
|
||||
* @param $action
|
||||
* @return SS_HTTPResponse
|
||||
*/
|
||||
protected function handleAction($request, $action) {
|
||||
$className = get_class($this);
|
||||
|
||||
if(!$this->hasMethod($action)) {
|
||||
return new SS_HTTPResponse("Action '$action' isn't available on class $className.", 404);
|
||||
}
|
||||
|
||||
$res = $this->extend('beforeCallActionHandler', $request, $action);
|
||||
if ($res) return reset($res);
|
||||
|
||||
$actionRes = $this->$action($request);
|
||||
|
||||
$res = $this->extend('afterCallActionHandler', $request, $action);
|
||||
if ($res) return reset($res);
|
||||
|
||||
return $actionRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a unified array of allowed actions on this controller (if such data is available) from both the controller
|
||||
* ancestry and any extensions.
|
||||
* Get a array of allowed actions defined on this controller,
|
||||
* any parent classes or extensions.
|
||||
*
|
||||
* Caution: Since 3.1, allowed_actions definitions only apply
|
||||
* to methods on the controller they're defined on,
|
||||
* so it is recommended to use the $class argument
|
||||
* when invoking this method.
|
||||
*
|
||||
* @param String $limitToClass
|
||||
* @return array|null
|
||||
*/
|
||||
public function allowedActions() {
|
||||
public function allowedActions($limitToClass = null) {
|
||||
if($limitToClass) {
|
||||
$actions = Config::inst()->get(
|
||||
$limitToClass,
|
||||
'allowed_actions',
|
||||
Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES
|
||||
);
|
||||
} else {
|
||||
$actions = Config::inst()->get(get_class($this), 'allowed_actions');
|
||||
}
|
||||
|
||||
$actions = Config::inst()->get(get_class($this), 'allowed_actions');
|
||||
if(is_array($actions)) {
|
||||
if(array_key_exists('*', $actions)) {
|
||||
Deprecation::notice(
|
||||
'3.0',
|
||||
'Wildcards (*) are no longer valid in $allowed_actions due their ambiguous '
|
||||
. ' and potentially insecure behaviour. Please define all methods explicitly instead.'
|
||||
);
|
||||
}
|
||||
|
||||
if($actions) {
|
||||
// convert all keys and values to lowercase to
|
||||
// allow for easier comparison, unless it is a permission code
|
||||
$actions = array_change_key_case($actions, CASE_LOWER);
|
||||
@ -248,35 +323,48 @@ class RequestHandler extends ViewableData {
|
||||
}
|
||||
|
||||
return $actions;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this request handler has a specific action (even if the current user cannot access it).
|
||||
* Checks if this request handler has a specific action,
|
||||
* even if the current user cannot access it.
|
||||
* Includes class ancestry and extensions in the checks.
|
||||
*
|
||||
* @param string $action
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAction($action) {
|
||||
if($action == 'index') return true;
|
||||
|
||||
// Don't allow access to any non-public methods (inspect instance plus all extensions)
|
||||
$insts = array_merge(array($this), (array)$this->getExtensionInstances());
|
||||
foreach($insts as $inst) {
|
||||
if(!method_exists($inst, $action)) continue;
|
||||
$r = new ReflectionClass(get_class($inst));
|
||||
$m = $r->getMethod($action);
|
||||
if(!$m || !$m->isPublic()) return false;
|
||||
}
|
||||
|
||||
$action = strtolower($action);
|
||||
$actions = $this->allowedActions();
|
||||
|
||||
// Check if the action is defined in the allowed actions as either a
|
||||
// key or value. Note that if the action is numeric, then keys are not
|
||||
// searched for actions to prevent actual array keys being recognised
|
||||
// as actions.
|
||||
// Check if the action is defined in the allowed actions of any ancestry class
|
||||
// as either a key or value. Note that if the action is numeric, then keys are not
|
||||
// searched for actions to prevent actual array keys being recognised as actions.
|
||||
if(is_array($actions)) {
|
||||
$isKey = !is_numeric($action) && array_key_exists($action, $actions);
|
||||
$isValue = in_array($action, $actions, true);
|
||||
$isWildcard = (in_array('*', $actions) && $this->checkAccessAction($action));
|
||||
if($isKey || $isValue || $isWildcard) return true;
|
||||
if($isKey || $isValue) return true;
|
||||
}
|
||||
|
||||
if(!is_array($actions) || !$this->config()->get('allowed_actions',
|
||||
Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES)) {
|
||||
|
||||
$actionsWithoutExtra = $this->config()->get(
|
||||
'allowed_actions',
|
||||
Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES
|
||||
);
|
||||
if(!is_array($actions) || !$actionsWithoutExtra) {
|
||||
if($action != 'init' && $action != 'run' && method_exists($this, $action)) return true;
|
||||
}
|
||||
|
||||
@ -289,60 +377,66 @@ class RequestHandler extends ViewableData {
|
||||
*/
|
||||
public function checkAccessAction($action) {
|
||||
$actionOrigCasing = $action;
|
||||
$action = strtolower($action);
|
||||
$allowedActions = $this->allowedActions();
|
||||
$action = strtolower($action);
|
||||
|
||||
if($allowedActions) {
|
||||
// check for specific action rules first, and fall back to global rules defined by asterisk
|
||||
foreach(array($action,'*') as $actionOrAll) {
|
||||
// check if specific action is set
|
||||
if(isset($allowedActions[$actionOrAll])) {
|
||||
$test = $allowedActions[$actionOrAll];
|
||||
if($test === true || $test === 1 || $test === '1') {
|
||||
// Case 1: TRUE should always allow access
|
||||
return true;
|
||||
} elseif(substr($test, 0, 2) == '->') {
|
||||
// Case 2: Determined by custom method with "->" prefix
|
||||
list($method, $arguments) = Object::parse_class_spec(substr($test, 2));
|
||||
return call_user_func_array(array($this, $method), $arguments);
|
||||
} else {
|
||||
// Case 3: Value is a permission code to check the current member against
|
||||
return Permission::check($test);
|
||||
}
|
||||
|
||||
} elseif((($key = array_search($actionOrAll, $allowedActions, true)) !== false) && is_numeric($key)) {
|
||||
// Case 4: Allow numeric array notation (search for array value as action instead of key)
|
||||
return true;
|
||||
}
|
||||
$isAllowed = false;
|
||||
$isDefined = false;
|
||||
if($this->hasMethod($actionOrigCasing) || !$action || $action == 'index') {
|
||||
// Get actions for this specific class (without inheritance)
|
||||
$definingClass = null;
|
||||
$insts = array_merge(array($this), (array)$this->getExtensionInstances());
|
||||
foreach($insts as $inst) {
|
||||
if(!method_exists($inst, $action)) continue;
|
||||
$r = new ReflectionClass(get_class($inst));
|
||||
$m = $r->getMethod($actionOrigCasing);
|
||||
$definingClass = $m->getDeclaringClass()->getName();
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here an the action is 'index', then it hasn't been specified, which means that
|
||||
// it should be allowed.
|
||||
if($action == 'index' || empty($action)) return true;
|
||||
|
||||
if($allowedActions === null || !$this->config()->get('allowed_actions',
|
||||
Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES)) {
|
||||
|
||||
// If no allowed_actions are provided, then we should only let through actions that aren't handled by
|
||||
// magic methods we test this by calling the unmagic method_exists.
|
||||
if(method_exists($this, $action)) {
|
||||
// Disallow any methods which aren't defined on RequestHandler or subclasses
|
||||
// (e.g. ViewableData->getSecurityID())
|
||||
$r = new ReflectionClass(get_class($this));
|
||||
if($r->hasMethod($actionOrigCasing)) {
|
||||
$m = $r->getMethod($actionOrigCasing);
|
||||
return ($m && is_subclass_of($m->getDeclaringClass()->getName(), 'RequestHandler'));
|
||||
|
||||
$allowedActions = $this->allowedActions($definingClass);
|
||||
|
||||
// check if specific action is set
|
||||
if(isset($allowedActions[$action])) {
|
||||
$isDefined = true;
|
||||
$test = $allowedActions[$action];
|
||||
if($test === true || $test === 1 || $test === '1') {
|
||||
// TRUE should always allow access
|
||||
$isAllowed = true;
|
||||
} elseif(substr($test, 0, 2) == '->') {
|
||||
// Determined by custom method with "->" prefix
|
||||
list($method, $arguments) = Object::parse_class_spec(substr($test, 2));
|
||||
$definingClassInst = Injector::inst()->get($definingClass);
|
||||
$isAllowed = call_user_func_array(array($definingClassInst, $method), $arguments);
|
||||
} else {
|
||||
throw new Exception("method_exists() true but ReflectionClass can't find method - PHP is b0kred");
|
||||
// Value is a permission code to check the current member against
|
||||
$isAllowed = Permission::check($test);
|
||||
}
|
||||
} else if(!$this->hasMethod($action)){
|
||||
// Return true so that a template can handle this action
|
||||
return true;
|
||||
} elseif(
|
||||
is_array($allowedActions)
|
||||
&& (($key = array_search($action, $allowedActions, true)) !== false)
|
||||
&& is_numeric($key)
|
||||
) {
|
||||
// Allow numeric array notation (search for array value as action instead of key)
|
||||
$isDefined = true;
|
||||
$isAllowed = true;
|
||||
} elseif(is_array($allowedActions) && !count($allowedActions)) {
|
||||
// If defined as empty array, deny action
|
||||
$isAllowed = false;
|
||||
} elseif($allowedActions === null) {
|
||||
// If undefined, allow action
|
||||
$isAllowed = true;
|
||||
}
|
||||
|
||||
// If we don't have a match in allowed_actions,
|
||||
// whitelist the 'index' action as well as undefined actions.
|
||||
if(!$isDefined && ($action == 'index' || empty($action))) {
|
||||
$isAllowed = true;
|
||||
}
|
||||
} else {
|
||||
// Doesn't have method, set to true so that a template can handle this action
|
||||
$isAllowed = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
return $isAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -415,7 +415,7 @@ class Session {
|
||||
protected function recursivelyApply($data, &$dest) {
|
||||
foreach($data as $k => $v) {
|
||||
if(is_array($v)) {
|
||||
if(!isset($dest[$k])) $dest[$k] = array();
|
||||
if(!isset($dest[$k]) || !is_array($dest[$k])) $dest[$k] = array();
|
||||
$this->recursivelyApply($v, $dest[$k]);
|
||||
} else {
|
||||
$dest[$k] = $v;
|
||||
@ -440,8 +440,8 @@ class Session {
|
||||
* @param type the type of message
|
||||
*/
|
||||
public static function setFormMessage($formname,$message,$type){
|
||||
Session::set("FormInfo.$formname.message", $message);
|
||||
Session::set("FormInfo.$formname.type", $type);
|
||||
Session::set("FormInfo.$formname.formError.message", $message);
|
||||
Session::set("FormInfo.$formname.formError.type", $type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -692,6 +692,7 @@ class Injector {
|
||||
* Register a service with an explicit name
|
||||
*/
|
||||
public function registerNamedService($name, $service) {
|
||||
$this->specs[$name] = array('class' => get_class($service));
|
||||
$this->serviceCache[$name] = $service;
|
||||
$this->inject($service);
|
||||
}
|
||||
@ -773,6 +774,9 @@ class Injector {
|
||||
if (isset($this->specs[$name])) {
|
||||
$spec = $this->specs[$name];
|
||||
$this->updateSpecConstructor($spec);
|
||||
if ($constructorArgs) {
|
||||
$spec['constructor'] = $constructorArgs;
|
||||
}
|
||||
return $this->instantiate($spec, $name);
|
||||
}
|
||||
}
|
||||
|
@ -437,7 +437,7 @@ abstract class Object {
|
||||
if(func_num_args() > 1) {
|
||||
Deprecation::notice('3.1.0', "Object::has_extension() deprecated. Call has_extension() on the class");
|
||||
$class = func_get_arg(0);
|
||||
$extension = func_get_arg(1);
|
||||
$requiredExtension = func_get_arg(1);
|
||||
}
|
||||
|
||||
$requiredExtension = strtolower($requiredExtension);
|
||||
|
@ -17,6 +17,8 @@ Used in side panels and action tabs
|
||||
|
||||
.backlink { padding-left: 12px; }
|
||||
|
||||
#Form_EditorToolbarMediaForm .ui-tabs-panel { padding-left: 0px; }
|
||||
|
||||
body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fieldset { padding: 16px; overflow: auto; background: #E2E2E2; }
|
||||
body.cms.ss-uploadfield-edit-iframe span.readonly, .composite.ss-assetuploadfield .details fieldset span.readonly { font-style: italic; color: #777777; text-shadow: 0px 1px 0px #fff; }
|
||||
body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-assetuploadfield .details fieldset .fieldholder-small label { margin-left: 0; }
|
||||
@ -36,7 +38,7 @@ body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-asse
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item .info { position: relative; padding: 7px; overflow: hidden; background-color: #FFBE66; border: 1px solid #FF9300; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-preview { position: absolute; height: 30px; width: 40px; overflow: hidden; z-index: 1; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-preview .no-preview { display: block; height: 100%; width: 100%; background: url("../images/icons/document.png") 2px 0px no-repeat; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-info { position: relative; line-height: 30px; font-size: 18px; overflow: hidden; background-color: #5db4df; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5db4df), color-stop(8%, #5db1dd), color-stop(50%, #439bcb), color-stop(54%, #3f99cd), color-stop(96%, #207db6), color-stop(100%, #1e7cba)); background-image: -webkit-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -moz-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -o-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-info { position: relative; line-height: 30px; font-size: 14px; overflow: hidden; background-color: #5db4df; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5db4df), color-stop(8%, #5db1dd), color-stop(50%, #439bcb), color-stop(54%, #3f99cd), color-stop(96%, #207db6), color-stop(100%, #1e7cba)); background-image: -webkit-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -moz-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -o-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info { background-color: #c11f1d; padding-right: 130px; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #c11f1d), color-stop(4%, #bf1d1b), color-stop(8%, #b71b1c), color-stop(15%, #b61e1d), color-stop(27%, #b11d1d), color-stop(31%, #ab1d1c), color-stop(42%, #a51b1b), color-stop(46%, #9f1b19), color-stop(50%, #9f1b19), color-stop(54%, #991c1a), color-stop(58%, #971a18), color-stop(62%, #911b1b), color-stop(65%, #911b1b), color-stop(88%, #7e1816), color-stop(92%, #771919), color-stop(100%, #731817)); background-image: -webkit-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: -moz-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: -o-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info .ss-uploadfield-item-name { width: 100%; cursor: default; background: #bcb9b9; background: rgba(201, 198, 198, 0.9); }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info .ss-uploadfield-item-name .name { text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.7); }
|
||||
@ -47,13 +49,13 @@ body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-asse
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name .ss-uploadfield-item-status.ui-state-error-text { max-width: 70%; position: absolute; right: 5px; text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.6); color: #cc0000; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name .ss-uploadfield-item-status.ui-state-warning-text { color: #b7a403; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name .ss-uploadfield-item-status.ui-state-success-text { color: #1f9433; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions { position: absolute; top: 0; right: 0; left: 0; z-index: 0; color: #f00; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions { position: absolute; top: 0; right: 0; left: 0; z-index: 0; color: #f00; font-size: 14px; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button { background: none; border: 0; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; text-shadow: none; color: white; float: right; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-delete { display: none; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel { -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; border-left: 1px solid rgba(255, 255, 255, 0.2); margin-top: 3px; cursor: pointer; opacity: 0.9; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel { -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; border-left: 1px solid rgba(255, 255, 255, 0.2); margin-top: 0px; cursor: pointer; opacity: 0.9; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel:hover { opacity: 1; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel .ui-icon { display: block; margin: 0; position: realtive; top: 4px; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit { opacity: 0.9; padding-top: 3px; padding-bottom: 0; height: 100%; -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel .ui-icon { display: block; margin: 0; position: realtive; top: 8px; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit { opacity: 0.9; padding-top: 1px; padding-bottom: 0; height: 100%; -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit.ui-state-hover { background: none; opacity: 1; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit.ui-state-hover span.toggle-details { opacity: 1; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit span.toggle-details { opacity: 0.9; margin-left: 3px; display: inline-block; width: 5px; height: 100%; cursor: pointer; }
|
||||
@ -66,16 +68,17 @@ body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-asse
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-progress .ss-uploadfield-item-progressbarvalue { width: 0; background: #60b3dd url(../images/progressbar_blue.gif) repeat left center; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-editform { /* don't use display none, for it will break jQuery('iframe').contents().height() */ height: 0; overflow: hidden; clear: both; }
|
||||
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-editform iframe { width: 100%; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { float: left; margin: 34px 0 0; }
|
||||
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { margin: 15px 0px 0 20px; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info label { font-size: 16px; line-height: 30px; padding: 5px 16px; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { float: left; margin: 10px 0 0; }
|
||||
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { margin: 10px 0px 0 20px; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info label { font-size: 18px; line-height: 30px; padding: 8px 16px; margin-right: 0px; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-fromcomputer { /*position: relative; */ overflow: hidden; display: block; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-uploador { float: left; font-weight: bold; font-size: 22px; padding: 0 20px; line-height: 70px; margin-top: 16px; display: none; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-fromcomputer .btn-icon-drive-upload-large { background: url(../images/drive-upload-large.png) no-repeat 0px -4px; width: 32px; height: 32px; margin-top: -12px; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-uploador { float: left; font-weight: bold; font-size: 22px; padding: 0 20px; line-height: 70px; margin-top: 4px; display: none; }
|
||||
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-uploador { font-size: 18px; margin-top: 0; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { margin-top: 9px; -webkit-border-radius: 13px; -moz-border-radius: 13px; -ms-border-radius: 13px; -o-border-radius: 13px; border-radius: 13px; -webkit-box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; -moz-box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; border: 2px dashed gray; background: #d4dbe0; display: none; height: 82px; width: 360px; float: left; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { margin-top: 9px; -webkit-border-radius: 13px; -moz-border-radius: 13px; -ms-border-radius: 13px; -o-border-radius: 13px; border-radius: 13px; -webkit-box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; -moz-box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; border: 2px dashed gray; background: #d4dbe0; display: none; height: 54px; width: 360px; float: left; text-align: center; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone.active.hover { -webkit-box-shadow: rgba(255, 255, 255, 0.6) 0 0 3px 2px inset; -moz-box-shadow: rgba(255, 255, 255, 0.6) 0 0 3px 2px inset; box-shadow: rgba(255, 255, 255, 0.6) 0 0 3px 2px inset; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { color: #5e5e5e; text-shadow: 0px 1px 0px #fff; background: url("../images/upload.png") 0 25px no-repeat; width: 230px; z-index: 1; padding: 20px 0 0; line-height: 25px; font-size: 25px; font-weight: bold; text-align: center; display: block; margin: 0 auto; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { display: block; font-size: 14px; z-index: -1; }
|
||||
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { margin-top: 3px; height: 56px; width: 277px; overflow: hidden; }
|
||||
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { background-position: 0 15px; }
|
||||
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { height: 30px; font-size: 18px; line-height: 18px; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { color: #5e5e5e; text-shadow: 0px 1px 0px #fff; background: url("../images/upload.png") 0 12px no-repeat; z-index: 1; padding: 20px 38px 0; line-height: 25px; font-size: 20px; font-weight: bold; display: inline-block; margin: 0 auto; }
|
||||
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { display: block; font-size: 12px; z-index: -1; margin-top: -3px; }
|
||||
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { height: 54px; width: 277px; overflow: hidden; }
|
||||
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { background-position: 0 13px; padding-top: 22px; }
|
||||
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { height: 38px; font-size: 18px; line-height: 18px; }
|
||||
|
@ -1,5 +1 @@
|
||||
.datetime .middleColumn .middleColumn { margin: 0; padding: 0; clear: none; float: left; }
|
||||
|
||||
.datetime .date .middleColumn { width: 20em; }
|
||||
|
||||
.datetime .time .middleColumn { width: 10em; }
|
||||
|
@ -115,7 +115,7 @@ Used in side panels and action tabs
|
||||
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination { padding-top: 1px; position: absolute; left: 50%; margin-left: -116px; z-index: 5; }
|
||||
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination .pagination-page-number { color: white; text-shadow: 0px -1px 0 rgba(0, 0, 0, 0.2); }
|
||||
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination .pagination-page-number input { width: 35px; height: 18px; margin-bottom: -6px; padding: 0px; border: 1px solid #899eab; border-bottom: 1px solid #a7b7c1; }
|
||||
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination button { -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; border: none; width: 10px; margin: 0 10px; }
|
||||
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination button { -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; border: none; width: 10px; margin: 0 10px; display: inline; float: none; }
|
||||
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination button span { text-indent: -9999em; }
|
||||
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination button.ss-gridfield-previouspage { background: url(../images/icons/pagination-arrows.png) no-repeat -23px 8px; }
|
||||
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination button.ss-gridfield-nextpage { background: url(../images/icons/pagination-arrows.png) no-repeat -47px 8px; }
|
||||
|
@ -223,6 +223,7 @@ class Debug {
|
||||
|
||||
public static function noticeHandler($errno, $errstr, $errfile, $errline, $errcontext) {
|
||||
if(error_reporting() == 0) return;
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
// Send out the error details to the logger for writing
|
||||
SS_Log::log(
|
||||
@ -237,7 +238,9 @@ class Debug {
|
||||
);
|
||||
|
||||
if(Director::isDev()) {
|
||||
self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Notice");
|
||||
return self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Notice");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -252,8 +255,10 @@ class Debug {
|
||||
*/
|
||||
public static function warningHandler($errno, $errstr, $errfile, $errline, $errcontext) {
|
||||
if(error_reporting() == 0) return;
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
if(self::$send_warnings_to) {
|
||||
self::emailError(self::$send_warnings_to, $errno, $errstr, $errfile, $errline, $errcontext, "Warning");
|
||||
return self::emailError(self::$send_warnings_to, $errno, $errstr, $errfile, $errline, $errcontext, "Warning");
|
||||
}
|
||||
|
||||
// Send out the error details to the logger for writing
|
||||
@ -273,8 +278,10 @@ class Debug {
|
||||
}
|
||||
|
||||
if(Director::isDev()) {
|
||||
self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Warning");
|
||||
}
|
||||
return self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Warning");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -289,6 +296,8 @@ class Debug {
|
||||
* @param unknown_type $errcontext
|
||||
*/
|
||||
public static function fatalHandler($errno, $errstr, $errfile, $errline, $errcontext) {
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
if(self::$send_errors_to) {
|
||||
self::emailError(self::$send_errors_to, $errno, $errstr, $errfile, $errline, $errcontext, "Error");
|
||||
}
|
||||
@ -310,11 +319,10 @@ class Debug {
|
||||
}
|
||||
|
||||
if(Director::isDev() || Director::is_cli()) {
|
||||
self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Error");
|
||||
return self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Error");
|
||||
} else {
|
||||
self::friendlyError();
|
||||
return self::friendlyError();
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -373,6 +381,7 @@ class Debug {
|
||||
$renderer->writeFooter();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -497,7 +506,7 @@ class Debug {
|
||||
$_SESSION['Security']['Message']['type'] = 'warning';
|
||||
$_SESSION['BackURL'] = $_SERVER['REQUEST_URI'];
|
||||
header($_SERVER['SERVER_PROTOCOL'] . " 302 Found");
|
||||
header("Location: " . Director::baseURL() . "Security/login");
|
||||
header("Location: " . Director::baseURL() . Security::login_url());
|
||||
die();
|
||||
}
|
||||
}
|
||||
@ -524,7 +533,7 @@ function exceptionHandler($exception) {
|
||||
$file = $exception->getFile();
|
||||
$line = $exception->getLine();
|
||||
$context = $exception->getTrace();
|
||||
Debug::fatalHandler($errno, $message, $file, $line, $context);
|
||||
return Debug::fatalHandler($errno, $message, $file, $line, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -543,21 +552,18 @@ function errorHandler($errno, $errstr, $errfile, $errline) {
|
||||
case E_ERROR:
|
||||
case E_CORE_ERROR:
|
||||
case E_USER_ERROR:
|
||||
Debug::fatalHandler($errno, $errstr, $errfile, $errline, null);
|
||||
break;
|
||||
return Debug::fatalHandler($errno, $errstr, $errfile, $errline, debug_backtrace());
|
||||
|
||||
case E_WARNING:
|
||||
case E_CORE_WARNING:
|
||||
case E_USER_WARNING:
|
||||
Debug::warningHandler($errno, $errstr, $errfile, $errline, null);
|
||||
break;
|
||||
return Debug::warningHandler($errno, $errstr, $errfile, $errline, debug_backtrace());
|
||||
|
||||
case E_NOTICE:
|
||||
case E_USER_NOTICE:
|
||||
case E_DEPRECATED:
|
||||
case E_USER_DEPRECATED:
|
||||
case E_STRICT:
|
||||
Debug::noticeHandler($errno, $errstr, $errfile, $errline, null);
|
||||
break;
|
||||
return Debug::noticeHandler($errno, $errstr, $errfile, $errline, debug_backtrace());
|
||||
}
|
||||
}
|
||||
|
@ -41,11 +41,8 @@ class SS_LogErrorEmailFormatter implements Zend_Log_Formatter_Interface {
|
||||
$data .= "<p style=\"color: white; background-color: $colour; margin: 0\">"
|
||||
. "[$errorType] $errstr<br />$errfile:$errline\n<br />\n<br />\n</p>\n";
|
||||
|
||||
// Get a backtrace, filtering out debug method calls
|
||||
$data .= SS_Backtrace::backtrace(true, false, array(
|
||||
'SS_LogErrorEmailFormatter->format',
|
||||
'SS_LogEmailWriter->_write'
|
||||
));
|
||||
// Render the provided backtrace
|
||||
$data .= SS_Backtrace::get_rendered_backtrace($errcontext);
|
||||
|
||||
// Compile extra data
|
||||
$blacklist = array('message', 'timestamp', 'priority', 'priorityName');
|
||||
|
@ -205,7 +205,15 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
|
||||
$className = get_class($this);
|
||||
$fixtureFile = eval("return {$className}::\$fixture_file;");
|
||||
|
||||
$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
|
||||
|
||||
// Set up email
|
||||
$this->originalMailer = Email::mailer();
|
||||
$this->mailer = new TestMailer();
|
||||
Email::set_mailer($this->mailer);
|
||||
Config::inst()->remove('Email', 'send_all_emails_to');
|
||||
Email::send_all_emails_to(null);
|
||||
|
||||
// Todo: this could be a special test model
|
||||
$this->model = DataModel::inst();
|
||||
@ -259,12 +267,6 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
$this->logInWithPermission("ADMIN");
|
||||
}
|
||||
|
||||
// Set up email
|
||||
$this->originalMailer = Email::mailer();
|
||||
$this->mailer = new TestMailer();
|
||||
Email::set_mailer($this->mailer);
|
||||
Email::send_all_emails_to(null);
|
||||
|
||||
// Preserve memory settings
|
||||
$this->originalMemoryLimit = ini_get('memory_limit');
|
||||
|
||||
|
@ -43,7 +43,7 @@ class TaskRunner extends Controller {
|
||||
echo "<ul>";
|
||||
foreach($tasks as $task) {
|
||||
echo "<li><p>";
|
||||
echo "<a href=\"{$base}dev/tasks/" . $task['class'] . "\">" . $task['title'] . "</a><br />";
|
||||
echo "<a href=\"{$base}dev/tasks/" . $task['segment'] . "\">" . $task['title'] . "</a><br />";
|
||||
echo "<span class=\"description\">" . $task['description'] . "</span>";
|
||||
echo "</p></li>\n";
|
||||
}
|
||||
@ -54,28 +54,41 @@ class TaskRunner extends Controller {
|
||||
} else {
|
||||
echo "SILVERSTRIPE DEVELOPMENT TOOLS: Tasks\n--------------------------\n\n";
|
||||
foreach($tasks as $task) {
|
||||
echo " * $task[title]: sake dev/tasks/" . $task['class'] . "\n";
|
||||
echo " * $task[title]: sake dev/tasks/" . $task['segment'] . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function runTask($request) {
|
||||
$taskName = $request->param('TaskName');
|
||||
if (class_exists($taskName) && is_subclass_of($taskName, 'BuildTask')) {
|
||||
$title = singleton($taskName)->getTitle();
|
||||
if(Director::is_cli()) echo "Running task '$title'...\n\n";
|
||||
elseif(!Director::is_ajax()) echo "<h1>Running task '$title'...</h1>\n";
|
||||
$name = $request->param('TaskName');
|
||||
$tasks = $this->getTasks();
|
||||
|
||||
$task = new $taskName();
|
||||
if ($task->isEnabled()) $task->run($request);
|
||||
else echo "<p>{$title} is disabled</p>";
|
||||
} else {
|
||||
echo "Build task '$taskName' not found.";
|
||||
if(class_exists($taskName)) echo " It isn't a subclass of BuildTask.";
|
||||
echo "\n";
|
||||
$title = function ($content) {
|
||||
printf(Director::is_cli() ? "%s\n\n" : '<h1>%s</h1>', $content);
|
||||
};
|
||||
|
||||
$message = function ($content) {
|
||||
printf(Director::is_cli() ? "%s\n" : '<p>%s</p>', $content);
|
||||
};
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
if ($task['segment'] == $name) {
|
||||
$inst = Injector::inst()->create($task['class']);
|
||||
$title(sprintf('Running Task %s', $inst->getTitle()));
|
||||
|
||||
if (!$inst->isEnabled()) {
|
||||
$message('The task is disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
$inst->run($request);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$message(sprintf('The build task "%s" could not be found', $name));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array Array of associative arrays for each task (Keys: 'class', 'title', 'description')
|
||||
*/
|
||||
@ -95,13 +108,14 @@ class TaskRunner extends Controller {
|
||||
$availableTasks[] = array(
|
||||
'class' => $class,
|
||||
'title' => singleton($class)->getTitle(),
|
||||
'segment' => str_replace('\\', '-', $class),
|
||||
'description' => $desc,
|
||||
);
|
||||
}
|
||||
|
||||
return $availableTasks;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -96,8 +96,12 @@ class TestSession {
|
||||
$form->setField(new SimpleByName($k), $v);
|
||||
}
|
||||
|
||||
if($button) $submission = $form->submitButton(new SimpleByName($button));
|
||||
else $submission = $form->submit();
|
||||
if($button) {
|
||||
$submission = $form->submitButton(new SimpleByName($button));
|
||||
if(!$submission) throw new Exception("Can't find button '$button' to submit as part of test.");
|
||||
} else {
|
||||
$submission = $form->submit();
|
||||
}
|
||||
|
||||
$url = Director::makeRelative($form->getAction()->asString());
|
||||
|
||||
@ -137,6 +141,15 @@ class TestSession {
|
||||
return $this->lastResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fake HTTP_REFERER; set each time get() or post() is called.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function lastUrl() {
|
||||
return $this->lastUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recent response's content
|
||||
*/
|
||||
|
@ -1277,11 +1277,20 @@ HTML;
|
||||
Deny from all
|
||||
</Files>
|
||||
|
||||
# This denies access to all yml files, since developers might include sensitive
|
||||
# information in them. See the docs for work-arounds to serve some yaml files
|
||||
<Files *.yml>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</Files>
|
||||
|
||||
ErrorDocument 404 /assets/error-404.html
|
||||
ErrorDocument 500 /assets/error-500.html
|
||||
|
||||
<IfModule mod_alias.c>
|
||||
RedirectMatch 403 /silverstripe-cache(/|$)
|
||||
RedirectMatch 403 /vendor(/|$)
|
||||
RedirectMatch 403 /composer\.(json|lock)
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
|
@ -116,7 +116,7 @@ Any arrays you pass as values to `update()` will be automatically merged. To rep
|
||||
|
||||
Note the different options for the third parameter of `get()`:
|
||||
|
||||
* `Config::INHERITED` will only get the configuration set for the specific class, not any of it's parents.
|
||||
* `Config::UNINHERITED` will only get the configuration set for the specific class, not any of it's parents.
|
||||
* `Config::FIRST_SET` will inherit configuration from parents, but stop on the first class that actually provides a value.
|
||||
* `Config::EXCLUDE_EXTRA_SOURCES` will not use additional static sources (such as those defined on extensions)
|
||||
|
||||
|
@ -1,12 +1,242 @@
|
||||
# 3.0.4
|
||||
# 3.0.4 (2013-02-19)
|
||||
|
||||
## Overview
|
||||
|
||||
* Security: Undefined or empty `$allowed_actions` overrides parent definitions
|
||||
* Security: Information leakage through web access on YAML configuration files
|
||||
* Security: Information leakage through web access on composer files
|
||||
* Security: Require ADMIN permissions for `?showtemplate=1`
|
||||
* Security: Reflected XSS in custom date/time formats in admin/security
|
||||
* Security: Stored XSS in the "New Group" dialog
|
||||
* Security: Reflected XSS in CMS status messages
|
||||
* API: More restrictive `$allowed_actions` checks for `Controller` when used with `Extension`
|
||||
* Changed `dev/tests/setdb` and `dev/tests/startsession` from session to cookie storage.
|
||||
|
||||
## Details
|
||||
|
||||
### Security: Undefined or empty `$allowed_actions` overrides parent definitions
|
||||
|
||||
Severity: Important
|
||||
|
||||
Description: `Controller` (and subclasses) failed to enforce `$allowed_action` restrictions
|
||||
on parent classes if a child class didn't have it explicitly defined, or it is set to an empty array.
|
||||
Since this is the default configuration on `Page_Controller`, most SilverStripe installations
|
||||
will be affected.
|
||||
|
||||
Impact: Depends on the used controller code. For any method with public visibility,
|
||||
the flaw can expose the return value of the method (unless it fails due to wrong arguments).
|
||||
It can also lead to unauthorized or unintended execution of logic, e.g. modifying the
|
||||
state of a database record.
|
||||
|
||||
Fix: Apply 3.0.4 update. In addition, we strongly recommend to define `$allowed_actions`
|
||||
on all controller classes to ensure the intentions are clearly communicated.
|
||||
Read more about `$allowed_actions` in our "[controller](/topics/controller/#access-control)"
|
||||
docs.
|
||||
|
||||
Reporter: Zann St Pierre
|
||||
|
||||
### Security: Information exposure through web access on YAML configuration files
|
||||
|
||||
Severity: Moderate
|
||||
|
||||
Description: YAML files are used to configure the SilverStripe application
|
||||
since its 3.0 release. These files can contain sensitive values such as database
|
||||
and API credentials. By default, the installer still stores database credentials
|
||||
in `_config.php` files which are safe from web access. So this only concerns
|
||||
configuration values added in your own project, or a third party module.
|
||||
|
||||
Resolution: Update your `.htaccess` file (for Apache), or your `web.config` file (for IIS)
|
||||
with the new files from the project root, and reapply any customizations you've made.
|
||||
Follow the [general upgrade instructions](/installation/upgrading).
|
||||
The [nginx installation instructions](/installation/nginx)
|
||||
have also been updated to reflect those changes.
|
||||
|
||||
### Security: Information exposure through web access on composer files
|
||||
|
||||
Severity: Low
|
||||
|
||||
Description: [Composer](http://getcomposer.org) is a dependency management
|
||||
tool which can optionally be used to install SilverStripe. The `composer.json`
|
||||
and `composer.lock` files are required for its operation, so they are included
|
||||
in the standard release since 3.0.2. These files contain information on the installed
|
||||
versions of core and thirdparty modules, which could be used to target specific
|
||||
versions of SilverStripe.
|
||||
|
||||
Resolution: Update your `.htaccess` file (for Apache), or your `web.config` file (for IIS)
|
||||
with the new files from the project root, and reapply any customizations you've made.
|
||||
Follow the [general upgrade instructions](/installation/upgrading).
|
||||
The [nginx installation instructions](/installation/nginx)
|
||||
have also been updated to reflect those changes.
|
||||
|
||||
|
||||
### Security: Require ADMIN permissions for `?showtemplate=1`
|
||||
|
||||
Severity: Low
|
||||
|
||||
Description: Avoids information leakage of compiled template data,
|
||||
which might expose some of the internal template logic.
|
||||
|
||||
### Security: Reflected XSS in custom date/time formats in admin/security
|
||||
|
||||
Severity: Low
|
||||
|
||||
Prerequisite: An attacker must have access to the admin interface.
|
||||
|
||||
Description: When creating a new user on the security page
|
||||
(Security->New User) within the admin interface, the user input
|
||||
is not properly validated and not encoded. A reflected XSS is
|
||||
possible within the `DateFormat_custom` and `TimeFormat_custom` fields.
|
||||
|
||||
Credits: Andreas Hunkeler (Compass Security AG, http://www.csnc.ch)
|
||||
|
||||
### Security: Stored XSS in the "New Group" dialog
|
||||
|
||||
Severity: Low
|
||||
|
||||
Prerequisite: An attacker must have access to the admin interface.
|
||||
|
||||
Description: There is a stored XSS vulnerability on the "group" tab on the
|
||||
security page in the admin interface
|
||||
(Security -> Groups -> New Group). It's possible to store a
|
||||
XSS within the group name. Everywhere where these group names
|
||||
are used, the XSS is executed. E.g. "New User" or "New Group".
|
||||
|
||||
Credits: Andreas Hunkeler (Compass Security AG, http://www.csnc.ch)
|
||||
|
||||
### Security: XSS in CMS status messages
|
||||
|
||||
Severity: Low
|
||||
|
||||
Prerequisite: An attacker must have access to the admin interface.
|
||||
|
||||
Description: Any data returned to CMS status messages (Growl-style popovers on top right)
|
||||
was not escaped, allowing XSS e.g. when publishing a page with
|
||||
a specifically crafted "Title" field.
|
||||
|
||||
Credits: Andreas Hunkeler (Compass Security AG, http://www.csnc.ch)
|
||||
|
||||
### API: More restrictive `$allowed_actions` checks for `Controller` when used with `Extension`
|
||||
|
||||
Controllers which are extended with `$allowed_actions` (through an `Extension`)
|
||||
now deny access to methods defined on the controller, unless this class also has them in its own
|
||||
`$allowed_actions` definition.
|
||||
|
||||
## Upgrading
|
||||
|
||||
* If you are using `dev/tests/setdb` and `dev/tests/startsession`,
|
||||
you'll need to configure a secure token in order to encrypt the cookie value:
|
||||
Simply run `sake dev/generatesecuretoken` and add the resulting code to your `mysite/_config.php`.
|
||||
Note that this functionality now requires the PHP `mcrypt` extension.
|
||||
Note that this functionality now requires the PHP `mcrypt` extension.
|
||||
|
||||
## Changelog
|
||||
|
||||
### API Changes
|
||||
|
||||
* 2013-02-15 [2352317](https://github.com/silverstripe/silverstripe-installer/commit/2352317) Filter composer files in IIS and Apache rules (fixes #8011) (Ingo Schommer)
|
||||
* 2013-02-12 [d969e29](https://github.com/silverstripe/sapphire/commit/d969e29) Require ADMIN for ?showtemplate=1 (Ingo Schommer)
|
||||
* 2013-02-12 [45c68d6](https://github.com/silverstripe/sapphire/commit/45c68d6) Require ADMIN for ?showtemplate=1 (Ingo Schommer)
|
||||
* 2013-01-23 [c69381c](https://github.com/silverstripe/sapphire/commit/c69381c) Remove Content-Length setting from HTTPResponse (fixes #8010) (Ingo Schommer)
|
||||
* 2012-12-11 [10d447e](https://github.com/silverstripe/silverstripe-installer/commit/10d447e) Removed 'make getallmodules', use composer instead (Ingo Schommer)
|
||||
* 2012-12-10 [efa9ff9](https://github.com/silverstripe/sapphire/commit/efa9ff9) Queries added by DataList::addInnerJoin() and DataList::leftJoin() come after the base joins, not before. (stojg)
|
||||
* 2012-12-06 [c6b1d4a](https://github.com/silverstripe/sapphire/commit/c6b1d4a) Storing alternative DB name in cookie rather than session (Ingo Schommer)
|
||||
* 2012-11-29 [f49f1ff](https://github.com/silverstripe/sapphire/commit/f49f1ff) Rename Transliterator to SS_Transliterator to remove conflict with Intl extension (Simon Welsh)
|
||||
* 2012-11-19 [96e56a8](https://github.com/silverstripe/silverstripe-installer/commit/96e56a8) Removed 'new-project' command (Ingo Schommer)
|
||||
* 2012-11-19 [8b68644](https://github.com/silverstripe/silverstripe-installer/commit/8b68644) Moved build tools to new silverstripe-buildtools module (Ingo Schommer)
|
||||
* 2012-11-09 [22095da](https://github.com/silverstripe/sapphire/commit/22095da) Hash autologin tokens before storing in the database. (Mateusz Uzdowski)
|
||||
* 2011-03-16 [d8bfc0b](https://github.com/silverstripe/sapphire/commit/d8bfc0b) Added Security::set_login_url() so that you can define an alternative log-in page if you have made one yourself. (Sam Minnee)
|
||||
* 2011-03-16 [f546979](https://github.com/silverstripe/sapphire/commit/f546979) Add a PermissionFailureException that can be thrown to trigger a log-in. (Sam Minnee)
|
||||
|
||||
### Features and Enhancements
|
||||
|
||||
* 2013-02-05 [f0621cd](https://github.com/silverstripe/sapphire/commit/f0621cd) Added ability to query size of Varchar (Daniel Hensby)
|
||||
* 2013-02-01 [119d8aa](https://github.com/silverstripe/silverstripe-cms/commit/119d8aa) Do not display SilverStripeNavigator_CMSLink when in a LeftAndMain extension not just CMSMain extensions (UndefinedOffset)
|
||||
* 2013-01-11 [5b450f7](https://github.com/silverstripe/sapphire/commit/5b450f7) Added replaceExistingFile setting for UploadField. (Sam Minnee)
|
||||
* 2013-01-11 [cc7318f](https://github.com/silverstripe/sapphire/commit/cc7318f) Added canAttachExisting config option for UploadField. (Sam Minnee)
|
||||
* 2013-01-10 [5e6f5f9](https://github.com/silverstripe/sapphire/commit/5e6f5f9) Allow configuration of send_all_emails_to, ccs_all_emails_to, and bcc_all_emails_to via the config system. (Sam Minnee)
|
||||
* 2013-01-09 [67c5db3](https://github.com/silverstripe/sapphire/commit/67c5db3) Global default config for UploadField (Ingo Schommer)
|
||||
* 2013-01-09 [2dfd427](https://github.com/silverstripe/sapphire/commit/2dfd427) Restrict upload abilities in UploadField (Ingo Schommer)
|
||||
* 2013-01-07 [abbee41](https://github.com/silverstripe/sapphire/commit/abbee41) Add ReadonlyField::setIncludeHiddenField() (Sam Minnee)
|
||||
* 2012-12-07 [e8fbfc0](https://github.com/silverstripe/sapphire/commit/e8fbfc0) FixtureFactory separated out from YamlFixture (Ingo Schommer)
|
||||
* 2012-11-15 [e07ae20](https://github.com/silverstripe/silverstripe-installer/commit/e07ae20) Added "phing phpunit" target (Ingo Schommer)
|
||||
* 2012-11-09 [32f829d](https://github.com/silverstripe/sapphire/commit/32f829d) Support for Behat tests, and initial set of tests (Ingo Schommer)
|
||||
* 2012-11-09 [9841d5b](https://github.com/silverstripe/silverstripe-cms/commit/9841d5b) Added Behat tests (Ingo Schommer)
|
||||
* 2012-10-24 [ea2dc9d](https://github.com/silverstripe/sapphire/commit/ea2dc9d) Add ability to change URL for SS logo in CMS Menu (Loz Calver)
|
||||
* 2012-10-16 [c4dde90](https://github.com/silverstripe/sapphire/commit/c4dde90) Allow hashes to be passed as ArrayList items; the will be turned into ArrayData objects. (Sam Minnee)
|
||||
* 2011-09-29 [2916f20](https://github.com/silverstripe/sapphire/commit/2916f20) Improve HTTP caching logic to automatically disable caching for requests that use the session. (Hamish Friedlander)
|
||||
* 2011-03-02 [c3a3ff4](https://github.com/silverstripe/sapphire/commit/c3a3ff4) Added Email::send_all_emails_from() setting. (Sam Minnee)
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* 2013-02-17 [ede3813](https://github.com/silverstripe/sapphire/commit/ede3813) Secure composer files from web access (fixes #8011) (Ingo Schommer)
|
||||
* 2013-02-17 [e21bd49](https://github.com/silverstripe/sapphire/commit/e21bd49) TimeField respects user choice (fixes #8260) (Ingo Schommer)
|
||||
* 2013-02-07 [79eacb2](https://github.com/silverstripe/sapphire/commit/79eacb2) Group->canEdit() correct non-admin checks (fixes #8250) (Ingo Schommer)
|
||||
* 2013-02-04 [857d8bb](https://github.com/silverstripe/sapphire/commit/857d8bb) Don't escape values on TreeDropdownField readonly views (Ingo Schommer)
|
||||
* 2013-02-04 [97fbfd3](https://github.com/silverstripe/silverstripe-cms/commit/97fbfd3) Respect escaping rules on readonly fields in CMS history view (Ingo Schommer)
|
||||
* 2013-02-04 [e56a78b](https://github.com/silverstripe/silverstripe-cms/commit/e56a78b) updateCMSFields not accepting var by reference (Michael Andrewartha)
|
||||
* 2013-02-04 [866bb07](https://github.com/silverstripe/sapphire/commit/866bb07) validate doesn't take var by reference (Michael Andrewartha)
|
||||
* 2013-02-04 [1960df8](https://github.com/silverstripe/sapphire/commit/1960df8) Strict error warnings on DataExtension (Michael Andrewartha)
|
||||
* 2013-01-31 [1bb1090](https://github.com/silverstripe/sapphire/commit/1bb1090) Node updates in IE without non-object error (Ingo Schommer)
|
||||
* 2013-01-30 [c9f728f](https://github.com/silverstripe/sapphire/commit/c9f728f) Only check the remember token if a user exists (Simon Welsh)
|
||||
* 2013-01-24 [f574979](https://github.com/silverstripe/sapphire/commit/f574979) Exception handling and email notification mechanism now correctly considers the stacktrace as provided by the exceptionHandler function, instead of attempting to perform a debug_backtrace further down the reporting chain (which ends up generating an unnecessarily nested stacktrace). Debug was cleaned up so that errorHandler and exceptionHandler both act consistently. As a result, the LogErrorEmailFormatter class could be simplified. (Damian Mooyman)
|
||||
* 2013-01-23 [45eb0f9](https://github.com/silverstripe/sapphire/commit/45eb0f9) PHPUnit latest not working with composer installed builds (Hamish Friedlander)
|
||||
* 2013-01-21 [5d37d55](https://github.com/silverstripe/sapphire/commit/5d37d55) Form session message clearing regression (Ingo Schommer)
|
||||
* 2013-01-18 [0c9b216](https://github.com/silverstripe/sapphire/commit/0c9b216) Escape the -f argument passed to mail() (Sam Minnee)
|
||||
* 2013-01-17 [e74ec57](https://github.com/silverstripe/sapphire/commit/e74ec57) Permission checkbox display on members (fixes #8193) (Ingo Schommer)
|
||||
* 2013-01-15 [a70df3e](https://github.com/silverstripe/sapphire/commit/a70df3e) PaginatedList deprecated method was calling non-existent method (Jeremy Thomerson)
|
||||
* 2013-01-15 [014f541](https://github.com/silverstripe/sapphire/commit/014f541) Regression in Form->clearMessage() (fixes #8186) (Ingo Schommer)
|
||||
* 2013-01-15 [64d3a3d](https://github.com/silverstripe/sapphire/commit/64d3a3d) Don't double unescape URLs in history.js (fixes #8170) (Ingo Schommer)
|
||||
* 2013-01-15 [420c639](https://github.com/silverstripe/sapphire/commit/420c639) Properly show link for showing and hiding class spec in model admin (jean)
|
||||
* 2013-01-15 [f06ba70](https://github.com/silverstripe/sapphire/commit/f06ba70) Undefined `$allowed_actions` overrides parent definitions, stricter handling of $allowed_actions on Extension (Ingo Schommer)
|
||||
* 2013-01-11 [e020c7b](https://github.com/silverstripe/sapphire/commit/e020c7b) doSave() and doDelete() should use translated singular name (uniun)
|
||||
* 2013-01-11 [f8758ba](https://github.com/silverstripe/sapphire/commit/f8758ba) Fixed margins so that margin is displayed between preview images and their title. (Sam Minnee)
|
||||
* 2013-01-11 [2fdd9a3](https://github.com/silverstripe/sapphire/commit/2fdd9a3) Allow images attached to UploadFields to be unlinked without File::canEdit() or File::canDelete() permission. (Sam Minnee)
|
||||
* 2013-01-11 [f4efaee](https://github.com/silverstripe/sapphire/commit/f4efaee) Fix DataObject::get_one() when the classname is passed with improper casing. (Sam Minnee)
|
||||
* 2013-01-06 [30096ee](https://github.com/silverstripe/sapphire/commit/30096ee) Keep Member.PasswordEncryption setting on empty passwords (Ingo Schommer)
|
||||
* 2013-01-04 [f8bbc0a](https://github.com/silverstripe/sapphire/commit/f8bbc0a) Escape HTML in DropdownField and ListboxField (Ingo Schommer)
|
||||
* 2013-01-04 [604ede3](https://github.com/silverstripe/sapphire/commit/604ede3) Escape HTML in CMS status messages (Ingo Schommer)
|
||||
* 2013-01-04 [7bb0bbf](https://github.com/silverstripe/sapphire/commit/7bb0bbf) Fixed XSS in admin/security and "My Profile" forms (Ingo Schommer)
|
||||
* 2012-12-21 [f0f83b2](https://github.com/silverstripe/sapphire/commit/f0f83b2) Graceful handling of sprintf with too few params in i18n::_t() (Ingo Schommer)
|
||||
* 2012-12-19 [22efd38](https://github.com/silverstripe/sapphire/commit/22efd38) Calling DataObject::relField() on a object with an empty relation list (Stig Lindqvist)
|
||||
* 2012-12-18 [6aba24b](https://github.com/silverstripe/sapphire/commit/6aba24b) removeRequiredField() should use array_splice() instead of unset() (uniun)
|
||||
* 2012-12-18 [d5a1c3d](https://github.com/silverstripe/sapphire/commit/d5a1c3d) SS has problems handling + in URLs. Filter them out. (Mateusz Uzdowski)
|
||||
* 2012-12-14 [55b611d](https://github.com/silverstripe/sapphire/commit/55b611d) Hardcoded project name in include_by_locale() (uniun)
|
||||
* 2012-12-13 [d42c004](https://github.com/silverstripe/silverstripe-cms/commit/d42c004) Fixed pagination functionality on root assets folder (Niklas Forsdahl)
|
||||
* 2012-12-12 [639cc02](https://github.com/silverstripe/sapphire/commit/639cc02) Fix insert media form inserting images from other UploadFields (fixes #8051) (Loz Calver)
|
||||
* 2012-12-11 [f431b35](https://github.com/silverstripe/sapphire/commit/f431b35) Confirmed Password Field now copies attributes to child fields. (Justin Martin)
|
||||
* 2012-12-07 [4f63f91](https://github.com/silverstripe/sapphire/commit/4f63f91) Fixed issue with convertServiceProperty (Marcus Nyeholt)
|
||||
* 2012-12-06 [1a4eaaa](https://github.com/silverstripe/sapphire/commit/1a4eaaa) Ensure has length before using string index access. (Simon Elvery)
|
||||
* 2012-12-05 [205ee42](https://github.com/silverstripe/sapphire/commit/205ee42) Make sure a message is set on ValidationException objects. (Simon Elvery)
|
||||
* 2012-12-05 [c0751df](https://github.com/silverstripe/silverstripe-cms/commit/c0751df) Remove handwritten SQL and use the ORM. (Mateusz Uzdowski)
|
||||
* 2012-12-04 [0be51a9](https://github.com/silverstripe/sapphire/commit/0be51a9) Fix ModelAdmin search (fixes #8052) (Ingo Schommer)
|
||||
* 2012-12-04 [1a4b245](https://github.com/silverstripe/sapphire/commit/1a4b245) Fix rewriteHashlinks in TabSet (Marcus Nyeholt)
|
||||
* 2012-12-04 [3478813](https://github.com/silverstripe/sapphire/commit/3478813) Rewrite hashlinks failing on empty a tags (Marcus Nyeholt)
|
||||
* 2012-11-26 [40a1a35](https://github.com/silverstripe/silverstripe-cms/commit/40a1a35) Namespaces for CmsFormsContext and CmsUiContext are wrong (Kirk Mayo)
|
||||
* 2012-11-23 [4310718](https://github.com/silverstripe/sapphire/commit/4310718) Insert Media, inserts all previous media (fixes #7545) (UndefinedOffset)
|
||||
* 2012-11-23 [453d04e](https://github.com/silverstripe/sapphire/commit/453d04e) Reset DataObject caches in SapphireTest->resetDBSchema() (Ingo Schommer)
|
||||
* 2012-11-23 [a3cd7dd](https://github.com/silverstripe/sapphire/commit/a3cd7dd) Force SapphireTest schema reset for extension changes (Ingo Schommer)
|
||||
* 2012-11-21 [41aec54](https://github.com/silverstripe/silverstripe-cms/commit/41aec54) Consistently use FormResponse in CMS JavaScript (fixes #8036) (Ingo Schommer)
|
||||
* 2012-11-20 [8f89aa9](https://github.com/silverstripe/sapphire/commit/8f89aa9) only call filemtime if file exists (Sander van Dragt)
|
||||
* 2012-11-16 [76c63fe](https://github.com/silverstripe/sapphire/commit/76c63fe) Fixed issue with SQLQuery::lastRow crashing on empty set. Added test cases for lastRow and firstRow. (Damian Mooyman)
|
||||
* 2012-11-15 [c6fcb08](https://github.com/silverstripe/sapphire/commit/c6fcb08) Video embed from Add Media Feature no longer works (open #8033) (stojg)
|
||||
* 2012-11-14 [91e48b8](https://github.com/silverstripe/silverstripe-cms/commit/91e48b8) Provide fallback text for translations. (Simon Elvery)
|
||||
* 2012-11-12 [2657a27](https://github.com/silverstripe/sapphire/commit/2657a27) Adjust the handler to jQuery UI 1.9 API change. (Mateusz Uzdowski)
|
||||
* 2012-11-12 [b6017a7](https://github.com/silverstripe/sapphire/commit/b6017a7) ArrayList now discards keys of the array passed in and keeps the numerically indexed array sequential. This fixes FirstLast and EvenOdd in templates, and makes ArrayList more consistent, as several methods already discarded the keys. (Andrew O'Neil)
|
||||
* 2012-11-12 [d58b23d](https://github.com/silverstripe/silverstripe-cms/commit/d58b23d) AssetAdmin filter array indices (fixes #8014) (Kirk Mayo)
|
||||
* 2012-11-09 [434759c](https://github.com/silverstripe/sapphire/commit/434759c) Correct redirection URL on deletion in GridFieldDetailForm (Ingo Schommer)
|
||||
* 2012-11-08 [6882635](https://github.com/silverstripe/sapphire/commit/6882635) Fixing non-object on file upload (Sean Harvey)
|
||||
* 2012-11-08 [6a69a2f](https://github.com/silverstripe/silverstripe-cms/commit/6a69a2f) Ensure required lang and css are loaded when using SiteTreeURLSegmentField (Simon Elvery)
|
||||
* 2012-02-09 [c048a01](https://github.com/silverstripe/sapphire/commit/c048a01) Avoid infinite redirection when logging out and when showing a custom login page after displaying the draft version of a page. (jean)
|
||||
* 2011-12-12 [1e1df8c](https://github.com/silverstripe/sapphire/commit/1e1df8c) Improved detection of empty HTMLText fields. (Sam Minnee)
|
||||
* 2011-09-30 [f41a7d8](https://github.com/silverstripe/sapphire/commit/f41a7d8) Fix issue with not being able to log out on Chrome when caching enabled because of Chrome bug (Hamish Friedlander)
|
||||
* 2011-09-01 [9a2ba48](https://github.com/silverstripe/sapphire/commit/9a2ba48) Made CSRF-error wording friendlier. (Sam Minnee)
|
||||
* 2011-08-31 [729bcc9](https://github.com/silverstripe/sapphire/commit/729bcc9) Don't clear form messages unless forTemplate() is actually called. BUGFIX: Clear session-stored form data as well as form error message. (Sam Minnee)
|
||||
* 2011-08-18 [5f9348b](https://github.com/silverstripe/sapphire/commit/5f9348b) Ensure that Security views respect redirections triggered by Page_Controller::init() (Sam Minnee)
|
||||
* 2011-07-07 [b114aa2](https://github.com/silverstripe/sapphire/commit/b114aa2) Added X-Forwarded-Protocol and User-Agent to Vary header. (Sam Minnee)
|
||||
* 2011-05-26 [55f3ec1](https://github.com/silverstripe/sapphire/commit/55f3ec1) Added error message fields to default search form (Jean-Fabien)
|
||||
* 2011-05-23 [7026a48](https://github.com/silverstripe/sapphire/commit/7026a48) for date manipulation use the SS_Datetime::now, otherwise it does not respect the mock date. (Mateusz Uzdowski)
|
||||
* 2011-05-21 [b7a1db7](https://github.com/silverstripe/sapphire/commit/b7a1db7) Set up the test mailer before loading the fixture, in case fixture-creation causes emails to be generated. (Sam Minnee)
|
||||
* 2011-04-29 [47e037e](https://github.com/silverstripe/sapphire/commit/47e037e) Removed notice-level error after forms w/ required fields are made readonly. (Sam Minnee)
|
||||
* 2011-04-20 [33a1fc7](https://github.com/silverstripe/sapphire/commit/33a1fc7) Fixed operation of inlined images in Mailer, when no inlined images actually attached. (Carlos Barberis)
|
||||
* 2011-04-18 [f8206d1](https://github.com/silverstripe/sapphire/commit/f8206d1) Prevent notice-level error in Session code when non-array is turned into an array. (Sam Minnee)
|
||||
* 2011-03-15 [6fcbad1](https://github.com/silverstripe/sapphire/commit/6fcbad1) Updated SilverStripe error handler so that log_errors still works. (Sam Minnee)
|
||||
* 2011-03-11 [82988d4](https://github.com/silverstripe/sapphire/commit/82988d4) Better error message when 401 response is corrupted. (Sam Minnee)
|
@ -19,6 +19,9 @@
|
||||
* Behaviour testing support through [Behat](http://behat.org), with CMS test coverage
|
||||
(see the [SilverStripe Behat Extension]() for details)
|
||||
* Removed legacy table APIs (e.g. `TableListField`), use GridField instead
|
||||
* Deny URL access if `Controller::$allowed_actions` is undefined
|
||||
* Removed support for "*" rules in `Controller::$allowed_actions`
|
||||
* Removed support for overriding rules on parent classes through `Controller::$allowed_actions`
|
||||
* Editing of relation table data (`$many_many_extraFields`) in `GridField`
|
||||
* Optional integration with ImageMagick as a new image manipulation backend
|
||||
* Support for PHP 5.4's built-in webserver
|
||||
@ -26,6 +29,78 @@
|
||||
|
||||
## Upgrading
|
||||
|
||||
### Deny URL access if `Controller::$allowed_actions` is undefined or empty array
|
||||
|
||||
In order to make controller access checks more consistent and easier to
|
||||
understand, the routing will require definition of `$allowed_actions`
|
||||
on your own `Controller` subclasses if they contain any actions
|
||||
accessible through URLs, or any forms.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
// This action is now denied because no $allowed_actions are specified
|
||||
public function myaction($request) {}
|
||||
}
|
||||
|
||||
Please review all rules governing allowed actions in the
|
||||
["controller" topic](/topics/controller).
|
||||
|
||||
### Removed support for "*" rules in `Controller::$allowed_actions`
|
||||
|
||||
The wildcard ('*') character allowed to define fallback rules
|
||||
in case they weren't explicitly defined. This caused a lot of confusion,
|
||||
particularly around inherited rules. We've decided to remove the feature,
|
||||
you'll need to specificy each accessible action individually.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
public static $allowed_actions = array('*' => 'ADMIN');
|
||||
// Always denied because not explicitly listed in $allowed_actions
|
||||
public function myaction($request) {}
|
||||
// Always denied because not explicitly listed in $allowed_actions
|
||||
public function myotheraction($request) {}
|
||||
}
|
||||
|
||||
Please review all rules governing allowed actions in the
|
||||
["controller" topic](/topics/controller).
|
||||
|
||||
### Removed support for overriding rules on parent classes through `Controller::$allowed_actions`
|
||||
|
||||
Since 3.1, the `$allowed_actions` definitions only apply
|
||||
to methods defined on the class they're also defined on.
|
||||
Overriding inherited access definitions is no longer possible.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
public static $allowed_actions = array('myaction' => 'ADMIN');
|
||||
public function myaction($request) {}
|
||||
}
|
||||
class MySubController extends MyController {
|
||||
// No longer works
|
||||
public static $allowed_actions = array('myaction' => 'CMS_ACCESS_CMSMAIN');
|
||||
}
|
||||
|
||||
This also applies for custom implementations of `handleAction()` and `handleRequest()`,
|
||||
which now have to be listed in the `$allowed_actions` specifically.
|
||||
It also restricts `Extension` classes applied to controllers, which now
|
||||
can only grant or deny access or methods they define themselves.
|
||||
|
||||
New approach with the [Config API](/topics/configuration)
|
||||
|
||||
:::php
|
||||
class MySubController extends MyController {
|
||||
public function init() {
|
||||
parent::init();
|
||||
|
||||
Config::inst()->update('MyController', 'allowed_actions',
|
||||
array('myaction' => 'CMS_ACCESS_CMSMAIN')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Please review all rules governing allowed actions in the
|
||||
["controller" topic](/topics/controller).
|
||||
|
||||
### Grouped CMS Buttons
|
||||
|
||||
The CMS buttons are now grouped, in order to hide minor actions by default and declutter the interface.
|
||||
|
@ -11,6 +11,8 @@ For information on how to upgrade to newer versions consult the [upgrading](/ins
|
||||
|
||||
* [3.1.0](3.1.0) - Unreleased
|
||||
|
||||
* [3.0.4](3.0.4) - 19 February 2013
|
||||
* [3.0.3](3.0.3) - 26 November 2012
|
||||
* [3.0.2](3.0.2) - 17 September 2012
|
||||
* [3.0.1](3.0.1) - 31 July 2012
|
||||
* [3.0.0](3.0.0) - 28 June 2012
|
||||
|
@ -68,6 +68,9 @@ You can have more customized logic and interface feedback through a custom contr
|
||||
:::php
|
||||
<?php
|
||||
class MyController extends Controller {
|
||||
|
||||
static $allowed_actions = array('Form');
|
||||
|
||||
protected $template = "BlankPage";
|
||||
|
||||
public function Link($action = null) {
|
||||
|
@ -9,6 +9,7 @@ Let's start by defining a new `ContactPage` page type:
|
||||
class ContactPage extends Page {
|
||||
}
|
||||
class ContactPage_Controller extends Page_Controller {
|
||||
static $allowed_actions = array('Form');
|
||||
public function Form() {
|
||||
$fields = new FieldList(
|
||||
new TextField('Name'),
|
||||
@ -60,6 +61,7 @@ Now that we have a contact form, we need some way of collecting the data submitt
|
||||
|
||||
:::php
|
||||
class ContactPage_Controller extends Page_Controller {
|
||||
static $allowed_actions = array('Form');
|
||||
public function Form() {
|
||||
// ...
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ configuration settings:
|
||||
index index.php index.html index.htm;
|
||||
|
||||
server_name example.com;
|
||||
|
||||
|
||||
include silverstripe3;
|
||||
include htaccess;
|
||||
}
|
||||
@ -29,7 +29,7 @@ Here is the include file `silverstripe3`:
|
||||
location / {
|
||||
try_files $uri @silverstripe;
|
||||
}
|
||||
|
||||
|
||||
location @silverstripe {
|
||||
include fastcgi_params;
|
||||
|
||||
@ -68,6 +68,11 @@ Here is the include file `htaccess`:
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# Block access to yaml files
|
||||
location ~ \.yml$ {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# cms & framework .htaccess rules
|
||||
location ~ ^/(cms|framework|mysite)/.*\.(php|php[345]|phtml|inc)$ {
|
||||
deny all;
|
||||
|
@ -26,4 +26,18 @@ name' and the default login details. Follow the questions and select the *instal
|
||||
|
||||
## Issues?
|
||||
|
||||
If the above steps don't work for any reason have a read of the [Common Problems](common-problems) section.
|
||||
If the above steps don't work for any reason have a read of the [Common Problems](common-problems) section.
|
||||
|
||||
## Security notes
|
||||
|
||||
### Yaml
|
||||
|
||||
For the reasons explained in [security](/topics/security) Yaml files are blocked by default by the .htaccess file
|
||||
provided by the SilverStripe installer module.
|
||||
|
||||
To allow serving yaml files from a specific directory, add code like this to an .htaccess file in that directory
|
||||
|
||||
<Files *.yml>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Files>
|
||||
|
@ -79,7 +79,7 @@ left-join for robustness; if there is no matching record in Page, we can return
|
||||
|
||||
SilverStripe has a powerful tool for automatically building database schemas. We've designed it so that you should never have to build them manually.
|
||||
|
||||
To access it, visit (site-root)/dev/build?flush=1. This script will analyze the existing schema, compare it to what's required by your data classes, and alter the schema as required.
|
||||
To access it, visit http://<mysite>/dev/build?flush=1. This script will analyze the existing schema, compare it to what's required by your data classes, and alter the schema as required.
|
||||
|
||||
Put the ?flush=1 on the end if you've added PHP files, so that the rest of the system will find these new classes.
|
||||
|
||||
|
@ -79,6 +79,7 @@ You can access the following controller-method with /team/signup
|
||||
class Team extends DataObject {}
|
||||
|
||||
class Team_Controller extends Controller {
|
||||
static $allowed_actions = array('signup');
|
||||
public function signup($id, $otherId) {
|
||||
return $this->renderWith('MyTemplate');
|
||||
}
|
||||
|
@ -41,6 +41,8 @@ Here is an example where we display a basic gridfield with the default settings:
|
||||
|
||||
:::php
|
||||
class GridController extends Page_Controller {
|
||||
|
||||
static $allowed_actions = array('index');
|
||||
|
||||
public function index(SS_HTTPRequest $request) {
|
||||
$this->Content = $this->AllPages();
|
||||
|
@ -22,6 +22,57 @@ _Versioned_. They are not general enough for using on any other DataObject. Tha
|
||||
of the feature.
|
||||
</div>
|
||||
|
||||
## Configuration and Defaults
|
||||
|
||||
Like most of the CMS, the preview UI is powered by
|
||||
[jQuery entwine](https://github.com/hafriedlander/jquery.entwine).
|
||||
This means its defaults are configured through JavaScript, by setting entwine properties.
|
||||
In order to achieve this, create a new file `mysite/javascript/MyLeftAndMain.Preview.js`.
|
||||
|
||||
In the following example we configure three aspects:
|
||||
|
||||
* Set the default mode from "split view" to a full "edit view"
|
||||
* Make a wider mobile preview
|
||||
* Increase minimum space required by preview before auto-hiding
|
||||
|
||||
Note how the configuration happens in different entwine namespaces
|
||||
("ss.preview" and "ss"), as well as applies to different selectors
|
||||
(".cms-preview" and ".cms-container").
|
||||
|
||||
:::js
|
||||
(function($) {
|
||||
$.entwine('ss.preview', function($){
|
||||
$('.cms-preview').entwine({
|
||||
DefaultMode: 'content',
|
||||
getSizes: function() {
|
||||
var sizes = this._super();
|
||||
sizes.mobile.width = '400px';
|
||||
return sizes;
|
||||
}
|
||||
});
|
||||
});
|
||||
$.entwine('ss', function($){
|
||||
$('.cms-container').entwine({
|
||||
getLayoutOptions: function() {
|
||||
var opts = this._super();
|
||||
opts.minPreviewWidth = 600;
|
||||
return opts;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}(jQuery));
|
||||
|
||||
Load the file in the CMS via an addition to `mysite/_config.php`:
|
||||
|
||||
:::php
|
||||
LeftAndMain::require_javascript('mysite/javascript/MyLeftAndMain.Preview.js');
|
||||
|
||||
In order to find out which configuration values are available, the source code
|
||||
is your best reference at the moment - have a look in `framework/admin/javascript/LeftAndMain.Preview.js`.
|
||||
To understand how layouts are handled in the CMS UI, have a look at the
|
||||
[CMS Architecture](/reference/cms-architecture) guide.
|
||||
|
||||
## Enabling preview
|
||||
|
||||
The frontend decides on the preview being enabled or disabled based on the presnce of the `.cms-previewable` class. If
|
||||
|
@ -56,6 +56,16 @@ By default it stores the generated file in the assets/ folder but you can config
|
||||
If SilverStripe doesn't have permissions on your server to write these files it will default back to including them
|
||||
individually .
|
||||
|
||||
You can also combine CSS files into a media-specific stylesheets as you would with the `Requirements::css` call - use
|
||||
the third paramter of the `combine_files` function:
|
||||
|
||||
:::php
|
||||
$printStylesheets = array(
|
||||
"$themeDir/css/print_HomePage.css",
|
||||
"$themeDir/css/print_Page.css",
|
||||
);
|
||||
Requirements::combine_files('print.css', $printStylesheets, 'print');
|
||||
|
||||
## Custom Inline Scripts
|
||||
|
||||
You can also quote custom script directly. This may seem a bit ugly, but is useful when you need to transfer some kind
|
||||
@ -183,4 +193,4 @@ slightly different JS/CSS requirements, the whole lot will be refetched.
|
||||
nature of an ajax-request. Needs some more research
|
||||
|
||||
## API Documentation
|
||||
`[api:Requirements]`
|
||||
`[api:Requirements]`
|
||||
|
163
docs/en/reference/shortcodes.md
Normal file
163
docs/en/reference/shortcodes.md
Normal file
@ -0,0 +1,163 @@
|
||||
# Shortcodes
|
||||
|
||||
The Shortcode API is a way to replace simple bbcode-like tags within HTML. It is inspired by and very similar to
|
||||
the [Wordpress implementation](http://codex.wordpress.org/Shortcode_API) of shortcodes.
|
||||
|
||||
A guide to syntax
|
||||
|
||||
Unclosed - [shortcode]
|
||||
Explicitly closed - [shortcode/]
|
||||
With parameters, mixed quoting - [shortcode parameter=value parameter2='value2' parameter3="value3"]
|
||||
Old style parameter separation - [shortcode,parameter=value,parameter2='value2',parameter3="value3"]
|
||||
With contained content & closing tag - [shortcode]Enclosed Content[/shortcode]
|
||||
Escaped (will output [just] [text] in response) - [[just] [[text]]
|
||||
|
||||
Shortcode parsing is already hooked into HTMLText and HTMLVarchar fields when rendered into a template
|
||||
|
||||
## Attribute and element scope
|
||||
|
||||
HTML with unprocessed shortcodes in it is still valid HTML. As a result, shortcodes can be in two places in HTML:
|
||||
|
||||
- In an attribute value, like so:
|
||||
|
||||
<a title="[title]">link</a>
|
||||
|
||||
- In an element's text, like so:
|
||||
|
||||
<p>
|
||||
Some text [shortcode] more text
|
||||
</p>
|
||||
|
||||
The first is called "element scope" use, the second "attribute scope"
|
||||
|
||||
You may not use shortcodes in any other location. Specifically, you can not use shortcodes to generate attributes or
|
||||
change the name of a tag. These usages are forbidden:
|
||||
|
||||
<[paragraph]>Some test</[paragraph]>
|
||||
|
||||
<a [titleattribute]>link</a>
|
||||
|
||||
Also note:
|
||||
|
||||
- you may need to escape text inside attributes `>` becomes `>` etc
|
||||
|
||||
- you can include HTML tags inside a shortcode tag, but you need to be careful of nesting to ensure you don't
|
||||
break the output
|
||||
|
||||
Good:
|
||||
|
||||
<div>
|
||||
[shortcode]
|
||||
<p>Caption</p>
|
||||
[/shortcode]
|
||||
</div>
|
||||
|
||||
Bad:
|
||||
|
||||
<div>
|
||||
[shortcode]
|
||||
</div>
|
||||
<p>
|
||||
[/shortcode]
|
||||
</p>
|
||||
|
||||
## Location
|
||||
|
||||
Element scoped shortcodes have a special ability to move the location they are inserted at to comply with
|
||||
HTML lexical rules. Take for example this basic paragraph tag:
|
||||
|
||||
<p><a href="#">Head [figure src="assets/a.jpg" caption="caption"] Tail</a></p>
|
||||
|
||||
When converted naively would become
|
||||
|
||||
<p><a href="#">Head <figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure> Tail</a></p>
|
||||
|
||||
However this is not valid HTML - P elements can not contain other block level elements.
|
||||
|
||||
To fix this you can specify a "location" attribute on a shortcode. When the location attribute is "left" or "right"
|
||||
the inserted content will be moved to immediately before the block tag. The result is this:
|
||||
|
||||
<figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure><p><a href="#">Head Tail</a></p>
|
||||
|
||||
When the location attribute is "leftAlone" or "center" then the DOM is split around the element. The result is this:
|
||||
|
||||
<p><a href="#">Head </a></p><figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure><p><a href="#"> Tail</a></p>
|
||||
|
||||
## Defining Custom Shortcodes
|
||||
|
||||
All you need to do to define a shortcode is to register a callback with the parser that will be called whenever a
|
||||
shortcode is encountered. This callback will return a string to replace the shortcode with.
|
||||
|
||||
:::php
|
||||
public static function my_shortcode_handler($attributes, $enclosedContent, $parser, $tagName) {
|
||||
// This simple callback simply converts the shortcode to a span.
|
||||
return "<span class=\"$tagName\">$enclosedContent</span>";
|
||||
}
|
||||
|
||||
The parameters passed to the callback are, in order:
|
||||
|
||||
* Any parameters attached to the shortcode as an associative array (keys are lower-case).
|
||||
* Any content enclosed within the shortcode (if it is an enclosing shortcode). Note that any content within this will
|
||||
not have been parsed, and can optionally be fed back into the parser.
|
||||
* The ShortcodeParser instance used to parse the content.
|
||||
* The shortcode tag name that was matched within the parsed content.
|
||||
|
||||
For the shortcode to work, you need to register it with the `ShortcodeParser`. Assuming you've placed the
|
||||
callback function in the `Page` class, you would need to make the following call from `_config.php`:
|
||||
|
||||
:::php
|
||||
ShortcodeParser::get('default')->register(
|
||||
'shortcode_tag_name',
|
||||
array('Page', 'my_shortcode_handler')
|
||||
);
|
||||
|
||||
An example result of installing such a shortcode would be that the string `[shortcode_tag_name]Testing
|
||||
testing[/shortcode_tag_name]` in the page *Content* would be replaced with the `<span class="shortcode_tag_name">Testing
|
||||
testing</span>`.
|
||||
|
||||
### Parameter values
|
||||
|
||||
Here is a summary of the callback parameter values based on some example shortcodes.
|
||||
|
||||
#### Short
|
||||
|
||||
[my_shortcodes]
|
||||
|
||||
$attributes => array()
|
||||
$enclosedContent => null
|
||||
$parser => ShortcodeParser instance
|
||||
$tagName => 'my_shortcode'
|
||||
|
||||
#### Short with attributes
|
||||
|
||||
[my_shortcode,attribute="foo",other="bar"]
|
||||
|
||||
$attributes => array ('attribute' => 'foo', 'other' => 'bar')
|
||||
$enclosedContent => null
|
||||
$parser => ShortcodeParser instance
|
||||
$tagName => 'my_shortcode'
|
||||
|
||||
#### Long with attributes
|
||||
|
||||
[my_shortcode,attribute="foo"]content[/my_shortcode]
|
||||
|
||||
$attributes => array('attribute' => 'foo')
|
||||
$enclosedContent => 'content'
|
||||
$parser => ShortcodeParser instance
|
||||
$tagName => 'my_shortcode'
|
||||
|
||||
## Inbuilt Shortcodes
|
||||
|
||||
All internal links inserted via the CMS into a content field are in the form `<a href="[sitetree_link,id=n]">`. At
|
||||
runtime this is replaced by a plain link to the page with the ID in question.
|
||||
|
||||
## Limitations
|
||||
|
||||
Since the shortcode parser is based on a simple regular expression it cannot properly handle nested shortcodes. For
|
||||
example the below code will not work as expected:
|
||||
|
||||
[shortcode]
|
||||
[shortcode][/shortcode]
|
||||
[/shortcode]
|
||||
|
||||
The parser will raise an error if it can not find a matching opening tag for any particular closing tag
|
@ -37,8 +37,8 @@ We'll explain some ways to use *SELECT* with the full power of SQL,
|
||||
but still maintain a connection to the ORM where possible.
|
||||
|
||||
<div class="warning" markdown="1">
|
||||
Please read our ["security" topic](/topics/security) to find out
|
||||
how to sanitize user input before using it in SQL queries.
|
||||
Please read our ["security" topic](/topics/security) to find out
|
||||
how to sanitize user input before using it in SQL queries.
|
||||
</div>
|
||||
|
||||
## Usage
|
||||
@ -140,4 +140,4 @@ An alternative approach would be a custom getter in the object definition.
|
||||
|
||||
* [datamodel](/topics/datamodel)
|
||||
* `[api:DataObject]`
|
||||
* [database-structure](database-structure)
|
||||
* [database-structure](database-structure)
|
||||
|
@ -568,6 +568,8 @@ default if it exists and there is no action in the url parameters.
|
||||
|
||||
:::php
|
||||
class MyPage_Controller extends Page_Controller {
|
||||
|
||||
static $allowed_actions = array('index');
|
||||
|
||||
public function init(){
|
||||
parent::init();
|
||||
|
@ -26,8 +26,8 @@ Append the option and corresponding value to your URL in your browser's address
|
||||
|
||||
| URL Variable | | Values | | Description |
|
||||
| ------------ | | ------ | | ----------- |
|
||||
| isDev | | 1 | | Put the site into [development mode](/topics/debugging), enabling debugging messages to the browser on a live server. For security, you'll be asked to log in with an administrator log-in |
|
||||
| isTest | | 1 | | Put the site into [test mode](/topics/debugging), enabling debugging messages to the admin email and generic errors to the browser on a live server |
|
||||
| isDev | | 1 | | Put the site into [development mode](/topics/debugging), enabling debugging messages to the browser on a live server. For security, you'll be asked to log in with an administrator log-in. Will persist for the current browser session. |
|
||||
| isTest | | 1 | | See above. |
|
||||
| debug | | 1 | | Show a collection of debugging information about the director / controller operation |
|
||||
| debug_request | | 1 | | Show all steps of the request from initial `[api:HTTPRequest]` to `[api:Controller]` to Template Rendering |
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
Base controller class. You will extend this to take granular control over the
|
||||
actions and url handling of aspects of your SilverStripe site.
|
||||
|
||||
## Example
|
||||
## Usage
|
||||
|
||||
The following example is for a simple `[api:Controller]` class. If you're using
|
||||
the cms module and looking at Page_Controller instances you won't need to setup
|
||||
@ -15,11 +15,14 @@ your own routes since the cms module handles these routes.
|
||||
<?php
|
||||
|
||||
class FastFood_Controller extends Controller {
|
||||
public function order($arguments) {
|
||||
public static $allowed_actions = array('order');
|
||||
public function order(SS_HTTPRequest $request) {
|
||||
print_r($arguments);
|
||||
}
|
||||
}
|
||||
|
||||
## Routing
|
||||
|
||||
`mysite/_config/routes.yml`
|
||||
|
||||
:::yaml
|
||||
@ -44,6 +47,100 @@ making any code changes to your controller.
|
||||
[Name] => cheesefries
|
||||
)
|
||||
|
||||
<div class="warning" markdown='1'>
|
||||
SilverStripe automatically adds a URL routing entry based on the controller's class name,
|
||||
so a `MyController` class is accessible through `http://yourdomain.com/MyController`.
|
||||
</div>
|
||||
|
||||
## Access Control
|
||||
|
||||
### Through $allowed_actions
|
||||
|
||||
All public methods on a controller are accessible by their name through the `$Action`
|
||||
part of the URL routing, so a `MyController->mymethod()` is accessible at
|
||||
`http://yourdomain.com/MyController/mymethod`. This is not always desireable,
|
||||
since methods can return internal information, or change state in a way
|
||||
that's not intended to be used through a URL endpoint.
|
||||
|
||||
SilverStripe strongly recommends securing your controllers
|
||||
through defining a `$allowed_actions` array on the class,
|
||||
which allows whitelisting of methods, as well as a concise
|
||||
way to perform checks against permission codes or custom logic.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
public static $allowed_actions = array(
|
||||
// someaction can be accessed by anyone, any time
|
||||
'someaction',
|
||||
// So can otheraction
|
||||
'otheraction' => true,
|
||||
// restrictedaction can only be people with ADMIN privilege
|
||||
'restrictedaction' => 'ADMIN',
|
||||
// complexaction can only be accessed if $this->canComplexAction() returns true
|
||||
'complexaction' '->canComplexAction'
|
||||
);
|
||||
}
|
||||
|
||||
There's a couple of rules guiding these checks:
|
||||
|
||||
* Each class is only responsible for access control on the methods it defines
|
||||
* If `$allowed_actions` is defined as an empty array, no actions are allowed
|
||||
* If `$allowed_actions` is undefined, all public methods on the specific class are allowed
|
||||
(not recommended)
|
||||
* Access checks on parent classes need to be overwritten via the Config API
|
||||
* Only public methods can be made accessible
|
||||
* If a method on a parent class is overwritten, access control for it has to be redefined as well
|
||||
* An action named "index" is whitelisted by default,
|
||||
unless allowed_actions is defined as an empty array,
|
||||
or the action is specifically restricted in there.
|
||||
* Methods returning forms also count as actions which need to be defined
|
||||
* Form action methods (targets of `FormAction`) should NOT be included in `$allowed_actions`,
|
||||
they're handled separately through the form routing (see the ["forms" topic](/topics/forms))
|
||||
* `$allowed_actions` can be defined on `Extension` classes applying to the controller.
|
||||
|
||||
If the permission check fails, SilverStripe will return a "403 Forbidden" HTTP status.
|
||||
|
||||
### Through the action
|
||||
|
||||
Each method responding to a URL can also implement custom permission checks,
|
||||
e.g. to handle responses conditionally on the passed request data.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
public static $allowed_actions = array('myaction');
|
||||
public function myaction($request) {
|
||||
if(!$request->getVar('apikey')) {
|
||||
return $this->httpError(403, 'No API key provided');
|
||||
}
|
||||
|
||||
return 'valid';
|
||||
}
|
||||
}
|
||||
|
||||
Unless you transform the response later in the request processing,
|
||||
it'll look pretty ugly to the user. Alternatively, you can use
|
||||
`ErrorPage::response_for(<status-code>)` to return a more specialized layout.
|
||||
|
||||
Note: This is recommended as an addition for `$allowed_actions`, in order to handle
|
||||
more complex checks, rather than a replacement.
|
||||
|
||||
### Through the init() method
|
||||
|
||||
After checking for allowed_actions, each controller invokes its `init()` method,
|
||||
which is typically used to set up common state in the controller, and
|
||||
include JavaScript and CSS files in the output which are used for any action.
|
||||
If an `init()` method returns a `SS_HTTPResponse` with either a 3xx or 4xx HTTP
|
||||
status code, it'll abort execution. This behaviour can be used to implement
|
||||
permission checks.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
public static $allowed_actions = array();
|
||||
public function init() {
|
||||
parent::init();
|
||||
if(!Permission::check('ADMIN')) return $this->httpError(403);
|
||||
}
|
||||
}
|
||||
|
||||
## URL Handling
|
||||
|
||||
@ -60,11 +157,26 @@ through `/fastfood/drivethrough/` to use the same order function.
|
||||
|
||||
:::php
|
||||
class FastFood_Controller extends Controller {
|
||||
|
||||
static $allowed_actions = array('drivethrough');
|
||||
public static $url_handlers = array(
|
||||
'drivethrough/$Action/$ID/$Name' => 'order'
|
||||
);
|
||||
|
||||
## Access Control
|
||||
|
||||
### Through $allowed_actions
|
||||
|
||||
* If `$allowed_actions` is undefined, `null` or `array()`, no actions are accessible
|
||||
* Each class is only responsible for access control on the methods it defines
|
||||
* Access checks on parent classes need to be overwritten via the Config API
|
||||
* Only public methods can be made accessible
|
||||
* If a method on a parent class is overwritten, access control for it has to be redefined as well
|
||||
* An action named "index" is whitelisted by default
|
||||
* Methods returning forms also count as actions which need to be defined
|
||||
* Form action methods (targets of `FormAction`) should NOT be included in `$allowed_actions`,
|
||||
they're handled separately through the form routing (see the ["forms" topic](/topics/forms))
|
||||
* `$allowed_actions` can be defined on `Extension` classes applying to the controller.
|
||||
|
||||
## URL Patterns
|
||||
|
||||
The `[api:RequestHandler]` class will parse all rules you specify against the
|
||||
|
@ -23,7 +23,8 @@ The SilverStripe database-schema is generated automatically by visiting the URL.
|
||||
`http://<mysite>/dev/build`
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
Note: You need to be logged in as an administrator to perform this command.
|
||||
Note: You need to be logged in as an administrator to perform this command,
|
||||
unless your site is in "[dev mode](/topics/debugging)", or the command is run through CLI.
|
||||
</div>
|
||||
|
||||
## Querying Data
|
||||
|
@ -56,10 +56,8 @@ of `$databaseConfig` and `Director::set_dev_servers`, and instead make sure that
|
||||
The mechanism by which the `_ss_environment.php` files work is quite simple. Here's how it works:
|
||||
|
||||
* At the beginning of SilverStripe's execution, the `_ss_environment.php` file is searched for, and if it is found, it's
|
||||
included. SilverStripe looks in 3 places for the file:
|
||||
* The site's base folder (ie, a sibling of framework, jsparty, and cms)
|
||||
* The parent of the base folder
|
||||
* The grandparent of the base folder
|
||||
included. SilverStripe looks in all the parent folders of framework up to the server root (using the REAL location of
|
||||
the dir - see PHP realpath()):
|
||||
* The `_ss_environment.php` file sets a number of "define()".
|
||||
* "conf/ConfigureFromEnv.php" is included from within your `mysite/_config.php`. This file has a number of regular
|
||||
configuration commands that use those defines as their arguments. If you are curious, open up
|
||||
|
@ -49,6 +49,7 @@ Example: Validate postcodes based on the selected country (on the controller).
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
static $allowed_actions = array('Form');
|
||||
public function Form() {
|
||||
return Form::create($this, 'Form',
|
||||
new FieldList(
|
||||
|
@ -25,9 +25,7 @@ Forms start at the controller. Here is an simple example on how to set up a form
|
||||
:::php
|
||||
class Page_Controller extends ContentController {
|
||||
|
||||
public static $allowed_actions = array(
|
||||
'HelloForm',
|
||||
);
|
||||
public static $allowed_actions = array('HelloForm');
|
||||
|
||||
// Template method
|
||||
public function HelloForm() {
|
||||
@ -41,11 +39,24 @@ Forms start at the controller. Here is an simple example on how to set up a form
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function doSayHello(array $data, Form $form) {
|
||||
public function doSayHello($data, Form $form) {
|
||||
// Do something with $data
|
||||
return $this->render();
|
||||
}
|
||||
}
|
||||
|
||||
The name of the form ("HelloForm") is passed into the `Form`
|
||||
constructor as a second argument. It needs to match the method name.
|
||||
|
||||
Since forms need a URL, the `HelloForm()` method needs to be handled
|
||||
like any other controller action. In order to whitelist its access through
|
||||
URLs, we add it to the `$allowed_actions` array.
|
||||
Form actions ("doSayHello") on the other hand should NOT be included here,
|
||||
these are handled separately through `Form->httpSubmission()`.
|
||||
You can control access on form actions either by conditionally removing
|
||||
a `FormAction` from the form construction,
|
||||
or by defining `$allowed_actions` in your own `Form` class
|
||||
(more information in the ["controllers" topic](/topics/controllers)).
|
||||
|
||||
**Page.ss**
|
||||
|
||||
|
@ -9,13 +9,13 @@ you can effectively select and upload files.
|
||||
|
||||
## Usage
|
||||
|
||||
The framework comes with a `[api:HTMLEditorField]` form field class which encapsulates most of the required functionality.
|
||||
It is usually added through the `[api:DataObject->getCMSFields()]` method:
|
||||
The framework comes with a `[api:HTMLEditorField]` form field class which encapsulates most of the required
|
||||
functionality. It is usually added through the `[api:DataObject->getCMSFields()]` method:
|
||||
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
static $db = array('Content' => 'HTMLText');
|
||||
|
||||
|
||||
public function getCMSFields() {
|
||||
return new FieldList(new HTMLEditorField('Content'));
|
||||
}
|
||||
@ -27,25 +27,128 @@ To keep the JavaScript editor configuration manageable and extensible,
|
||||
we've wrapped it in a PHP class called `[api:HtmlEditorConfig]`.
|
||||
The class comes with its own defaults, which are extended through the `_config.php`
|
||||
files in the framework (and the `cms` module in case you've got that installed).
|
||||
There can be multiple configs, which should always be created / accessed using `[api:HtmlEditorConfig::get].
|
||||
You can then set the currently active config using `set_active()`.
|
||||
There can be multiple configs, which should always be created / accessed using `[api:HtmlEditorConfig::get]`.
|
||||
You can then set the currently active config using `set_active()`.
|
||||
By default, a config named 'cms' is used in any field created throughout the CMS interface.
|
||||
|
||||
Example: Enable the "media" plugin:
|
||||
<div class="notice" markdown='1'>
|
||||
Caveat: currently the order in which the `_config.php` files are executed depends on the module directory
|
||||
names. Execution order is alphabetical, so if you set a TinyMCE option in the `aardvark/_config.php`, this
|
||||
will be overriden in `framework/admin/_config.php` and your modification will disappear.
|
||||
|
||||
This is a general problem with `_config.php` files - it may be fixed in the future by making it possible to
|
||||
configure the TinyMCE with the new [configuration system](../topics/configuration).
|
||||
</div>
|
||||
|
||||
### Adding and removing capabilities
|
||||
|
||||
In its simplest form, the configuration of the editor includes adding and removing buttons and plugins.
|
||||
|
||||
You can add plugins to the editor using the Framework's `[api:HtmlEditorConfig::enablePlugins]` method. This will
|
||||
transparently generate the relevant underlying TinyMCE code.
|
||||
|
||||
:::php
|
||||
// File: mysite/_config.php
|
||||
HtmlEditorConfig::get('cms')->enablePlugins('media');
|
||||
|
||||
Example: Remove some buttons for more advanced formatting
|
||||
Note: this utilises the TinyMCE's `PluginManager::load` function under the hood (check the
|
||||
[TinyMCE documentation on plugin
|
||||
loading](http://www.tinymce.com/wiki.php/API3:method.tinymce.AddOnManager.load) for details).
|
||||
|
||||
Plugins and advanced themes can provide additional buttons that can be added (or removed) through the
|
||||
configuration. Here is an example of adding a `ssmacron` button after the `charmap` button:
|
||||
|
||||
:::php
|
||||
// File: mysite/_config.php
|
||||
HtmlEditorConfig::get('cms')->insertButtonsAfter('charmap', 'ssmacron');
|
||||
|
||||
Buttons can also be removed:
|
||||
|
||||
:::php
|
||||
// File: mysite/_config.php
|
||||
HtmlEditorConfig::get('cms')->removeButtons('tablecontrols', 'blockquote', 'hr');
|
||||
|
||||
Note: internally `[api:HtmlEditorConfig]` uses the TinyMCE's `theme_advanced_buttons` option to configure these. See
|
||||
the [TinyMCE documentation of this option](http://www.tinymce.com/wiki.php/Configuration:theme_advanced_buttons_1_n)
|
||||
for more details.
|
||||
|
||||
### Setting options
|
||||
|
||||
TinyMCE behaviour can be affected through its [configuration options](http://www.tinymce.com/wiki.php/Configuration).
|
||||
These options will be passed straight to the editor.
|
||||
|
||||
One example of the usage of this capability is to redefine the TinyMCE's [whitelist of HTML
|
||||
tags](http://www.tinymce.com/wiki.php/Configuration:extended_valid_elements) - the tags that will not be stripped
|
||||
from the HTML source by the editor.
|
||||
|
||||
:::php
|
||||
// Add start and type attributes for <ol>, add <object> and <embed> with all attributes.
|
||||
HtmlEditorConfig::get('cms')->setOption(
|
||||
'extended_valid_elements',
|
||||
'img[class|src|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name|usemap],' .
|
||||
'iframe[src|name|width|height|title|align|allowfullscreen|frameborder|marginwidth|marginheight|scrolling],' .
|
||||
'object[classid|codebase|width|height|data|type],' .
|
||||
'embed[src|type|pluginspage|width|height|autoplay],' .
|
||||
'param[name|value],' .
|
||||
'map[class|name|id],' .
|
||||
'area[shape|coords|href|target|alt],' .
|
||||
'ol[start|type]'
|
||||
);
|
||||
|
||||
Note: the default setting for the CMS's `extended_valid_elements` we are overriding here can be found in
|
||||
`framework/admin/_config.php`.
|
||||
|
||||
### Writing custom plugins
|
||||
|
||||
It is also possible to add custom buttons to TinyMCE. A simple example of this is SilverStripe's `ssmacron`
|
||||
plugin. The source can be found in the Framework's `thirdparty/tinymce_ssmacron` directory.
|
||||
|
||||
Here is how we can create a project-specific plugin. Create a `mysite/javascript/myplugin` directory,
|
||||
add the plugin button icon - here `myplugin.png` - and the source code - here `editor_plugin.js`. Here is a very
|
||||
simple example of a plugin that adds a button to the editor:
|
||||
|
||||
:::js
|
||||
(function() {
|
||||
tinymce.create('tinymce.plugins.myplugin', {
|
||||
|
||||
init : function(ed, url) {
|
||||
var self = this;
|
||||
|
||||
ed.addButton ('myplugin', {
|
||||
'title' : 'My plugin',
|
||||
'image' : url+'/myplugin.png',
|
||||
'onclick' : function () {
|
||||
alert('Congratulations! Your plugin works!');
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
getInfo : function() {
|
||||
return {
|
||||
longname : 'myplugin',
|
||||
author : 'Me',
|
||||
authorurl : 'http://me.org.nz/',
|
||||
infourl : 'http://me.org.nz/myplugin/',
|
||||
version : "1.0"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
tinymce.PluginManager.add('myplugin', tinymce.plugins.myplugin);
|
||||
})();
|
||||
|
||||
You can then enable this plugin through the `[api:HtmlEditorConfig::enablePlugins]`:
|
||||
|
||||
:::php
|
||||
HtmlEditorConfig::get('cms')->enablePlugins(array('myplugin' => '../../../mysite/javascript/myplugin/editor_plugin.js'));
|
||||
|
||||
For more complex examples see the [Creating a Plugin](http://www.tinymce.com/wiki.php/Creating_a_plugin) in TinyMCE
|
||||
documentation, or browse through plugins that come with the Framework at `thirdparty/tinymce/plugins`.
|
||||
|
||||
## Image and Media Insertion
|
||||
|
||||
The `[api:HtmlEditorField]` API also handles inserting images and media
|
||||
The `[api:HtmlEditorField]` API also handles inserting images and media
|
||||
files into the managed HTML content. It can be used both for referencing
|
||||
files on the webserver filesystem (through the `[api:File]` and `[api:Image]` APIs),
|
||||
as well as hotlinking files from the web.
|
||||
@ -60,7 +163,8 @@ its URL, as opposed to dealing with manual HTML code.
|
||||
oEmbed powers the "Insert from web" feature available through `[api:HtmlEditorField]`.
|
||||
Internally, it makes HTTP queries to a list of external services
|
||||
if it finds a matching URL. These services are described in the `Oembed.providers` configuration.
|
||||
Since these requests are performed on page rendering, they typically have a long cache time (multiple days). To refresh a cache, append `?flush=1` to a URL.
|
||||
Since these requests are performed on page rendering, they typically have a long cache time (multiple days). To refresh
|
||||
a cache, append `?flush=1` to a URL.
|
||||
|
||||
To disable oEmbed usage, set the `Oembed.enabled` configuration property to "false".
|
||||
|
||||
|
@ -79,6 +79,7 @@ Example:
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
static $allowed_actions = array('myurlaction');
|
||||
public function myurlaction($RAW_urlParams) {
|
||||
$SQL_urlParams = Convert::raw2sql($RAW_urlParams); // works recursively on an array
|
||||
$objs = Player::get()->where("Name = '{$SQL_data[OtherID]}'");
|
||||
@ -93,7 +94,6 @@ This means if you've got a chain of functions passing data through, escaping sho
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
/**
|
||||
|
||||
* @param array $RAW_data All names in an indexed array (not SQL-safe)
|
||||
*/
|
||||
public function saveAllNames($RAW_data) {
|
||||
@ -220,6 +220,7 @@ PHP:
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
static $allowed_actions = array('search');
|
||||
public function search($request) {
|
||||
$htmlTitle = '<p>Your results for:' . Convert::raw2xml($request->getVar('Query')) . '</p>';
|
||||
return $this->customise(array(
|
||||
@ -249,6 +250,7 @@ PHP:
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
static $allowed_actions = array('search');
|
||||
public function search($request) {
|
||||
$rssRelativeLink = "/rss?Query=" . urlencode($_REQUEST['query']) . "&sortOrder=asc";
|
||||
$rssLink = Controller::join_links($this->Link(), $rssRelativeLink);
|
||||
@ -363,6 +365,16 @@ file in the assets directory. This requires PHP to be loaded as an Apache modul
|
||||
php_flag engine off
|
||||
Options -ExecCGI -Includes -Indexes
|
||||
|
||||
### Don't allow access to .yml files
|
||||
|
||||
Yaml files are often used to store sensitive or semi-sensitive data for use by SilverStripe framework (for instance,
|
||||
configuration and test fixtures).
|
||||
|
||||
You should therefore block access to all yaml files (extension .yml) by default, and white list only yaml files
|
||||
you need to serve directly.
|
||||
|
||||
See [Apache](/installation/webserver) and [Nginx](/installation/nginx) installation documentation for details
|
||||
specific to your web server
|
||||
|
||||
## Related
|
||||
|
||||
|
@ -21,6 +21,8 @@ The poll we will be creating on our homepage will ask the user for their name an
|
||||
|
||||
:::php
|
||||
class HomePage_Controller extends Page_Controller {
|
||||
static $allowed_actions = array('BrowserPollForm');
|
||||
|
||||
// ...
|
||||
|
||||
public function BrowserPollForm() {
|
||||
|
@ -118,16 +118,54 @@ class Email extends ViewableData {
|
||||
static $admin_email_address = '';
|
||||
|
||||
/**
|
||||
* Send every email generated by the Email class to the given address.
|
||||
*
|
||||
* It will also add " [addressed to (email), cc to (email), bcc to (email)]" to the end of the subject line
|
||||
*
|
||||
* To set this, set Email.send_all_emails_to in your yml config file.
|
||||
* It can also be set in _ss_environment.php with SS_SEND_ALL_EMAILS_TO.
|
||||
*
|
||||
* @param string $send_all_emails_to Email-Address
|
||||
*/
|
||||
protected static $send_all_emails_to = null;
|
||||
|
||||
/**
|
||||
* Send every email generated by the Email class *from* the given address.
|
||||
* It will also add " [, from to (email)]" to the end of the subject line
|
||||
*
|
||||
* To set this, set Email.send_all_emails_from in your yml config file.
|
||||
* It can also be set in _ss_environment.php with SS_SEND_ALL_EMAILS_FROM.
|
||||
*
|
||||
* @param string $send_all_emails_from Email-Address
|
||||
*/
|
||||
protected static $send_all_emails_from = null;
|
||||
|
||||
/**
|
||||
* BCC every email generated by the Email class to the given address.
|
||||
* It won't affect the original delivery in the same way that send_all_emails_to does. It just adds a BCC header
|
||||
* with the given email address. Note that you can only call this once - subsequent calls will overwrite the
|
||||
* configuration variable.
|
||||
*
|
||||
* This can be used when you have a system that relies heavily on email and you want someone to be checking all
|
||||
* correspondence.
|
||||
*
|
||||
* To set this, set Email.bcc_all_emails_to in your yml config file.
|
||||
*
|
||||
* @param string $bcc_all_emails_to Email-Address
|
||||
*/
|
||||
protected static $bcc_all_emails_to = null;
|
||||
|
||||
/**
|
||||
* CC every email generated by the Email class to the given address.
|
||||
* It won't affect the original delivery in the same way that send_all_emails_to does. It just adds a CC header
|
||||
* with the given email address. Note that you can only call this once - subsequent calls will overwrite the
|
||||
* configuration variable.
|
||||
*
|
||||
* This can be used when you have a system that relies heavily on email and you want someone to be checking all
|
||||
* correspondence.
|
||||
*
|
||||
* To set this, set Email.cc_all_emails_to in your yml config file.
|
||||
*
|
||||
* @param string $cc_all_emails_to Email-Address
|
||||
*/
|
||||
protected static $cc_all_emails_to = null;
|
||||
@ -388,37 +426,45 @@ class Email extends ViewableData {
|
||||
if(project()) $headers['X-SilverStripeSite'] = project();
|
||||
|
||||
$to = $this->to;
|
||||
$from = $this->from;
|
||||
$subject = $this->subject;
|
||||
if(self::$send_all_emails_to) {
|
||||
if($sendAllTo = $this->config()->send_all_emails_to) {
|
||||
$subject .= " [addressed to $to";
|
||||
$to = self::$send_all_emails_to;
|
||||
$to = $sendAllTo;
|
||||
if($this->cc) $subject .= ", cc to $this->cc";
|
||||
if($this->bcc) $subject .= ", bcc to $this->bcc";
|
||||
$subject .= ']';
|
||||
unset($headers['Cc']);
|
||||
unset($headers['Bcc']);
|
||||
} else {
|
||||
if($this->cc) $headers['Cc'] = $this->cc;
|
||||
if($this->bcc) $headers['Bcc'] = $this->bcc;
|
||||
}
|
||||
|
||||
if(self::$cc_all_emails_to) {
|
||||
if($ccAllTo = $this->config()->cc_all_emails_to) {
|
||||
if(!empty($headers['Cc']) && trim($headers['Cc'])) {
|
||||
$headers['Cc'] .= ', ' . self::$cc_all_emails_to;
|
||||
$headers['Cc'] .= ', ' . $ccAllTo;
|
||||
} else {
|
||||
$headers['Cc'] = self::$cc_all_emails_to;
|
||||
$headers['Cc'] = $ccAllTo;
|
||||
}
|
||||
}
|
||||
|
||||
if(self::$bcc_all_emails_to) {
|
||||
if($bccAllTo = $this->config()->bcc_all_emails_to) {
|
||||
if(!empty($headers['Bcc']) && trim($headers['Bcc'])) {
|
||||
$headers['Bcc'] .= ', ' . self::$bcc_all_emails_to;
|
||||
$headers['Bcc'] .= ', ' . $bccAllTo;
|
||||
} else {
|
||||
$headers['Bcc'] = self::$bcc_all_emails_to;
|
||||
$headers['Bcc'] = $bccAllTo;
|
||||
}
|
||||
}
|
||||
|
||||
if($sendAllfrom = $this->config()->send_all_emails_from) {
|
||||
if($from) $subject .= " [from $from]";
|
||||
$from = $sendAllfrom;
|
||||
}
|
||||
|
||||
Requirements::restore();
|
||||
|
||||
return self::mailer()->sendPlain($to, $this->from, $subject, $this->body, $this->attachments, $headers);
|
||||
return self::mailer()->sendPlain($to, $from, $subject, $this->body, $this->attachments, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -444,40 +490,49 @@ class Email extends ViewableData {
|
||||
|
||||
if(project()) $headers['X-SilverStripeSite'] = project();
|
||||
|
||||
|
||||
$to = $this->to;
|
||||
$from = $this->from;
|
||||
$subject = $this->subject;
|
||||
if(self::$send_all_emails_to) {
|
||||
if($sendAllTo = $this->config()->send_all_emails_to) {
|
||||
$subject .= " [addressed to $to";
|
||||
$to = self::$send_all_emails_to;
|
||||
$to = $sendAllTo;
|
||||
if($this->cc) $subject .= ", cc to $this->cc";
|
||||
if($this->bcc) $subject .= ", bcc to $this->bcc";
|
||||
$subject .= ']';
|
||||
unset($headers['Cc']);
|
||||
unset($headers['Bcc']);
|
||||
|
||||
} else {
|
||||
if($this->cc) $headers['Cc'] = $this->cc;
|
||||
if($this->bcc) $headers['Bcc'] = $this->bcc;
|
||||
}
|
||||
|
||||
if(self::$cc_all_emails_to) {
|
||||
|
||||
if($ccAllTo = $this->config()->cc_all_emails_to) {
|
||||
if(!empty($headers['Cc']) && trim($headers['Cc'])) {
|
||||
$headers['Cc'] .= ', ' . self::$cc_all_emails_to;
|
||||
$headers['Cc'] .= ', ' . $ccAllTo;
|
||||
} else {
|
||||
$headers['Cc'] = self::$cc_all_emails_to;
|
||||
$headers['Cc'] = $ccAllTo;
|
||||
}
|
||||
}
|
||||
|
||||
if(self::$bcc_all_emails_to) {
|
||||
|
||||
if($bccAllTo = $this->config()->bcc_all_emails_to) {
|
||||
if(!empty($headers['Bcc']) && trim($headers['Bcc'])) {
|
||||
$headers['Bcc'] .= ', ' . self::$bcc_all_emails_to;
|
||||
$headers['Bcc'] .= ', ' . $bccAllTo;
|
||||
} else {
|
||||
$headers['Bcc'] = self::$bcc_all_emails_to;
|
||||
$headers['Bcc'] = $bccAllTo;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if($sendAllfrom = $this->config()->send_all_emails_from) {
|
||||
if($from) $subject .= " [from $from]";
|
||||
$from = $sendAllfrom;
|
||||
}
|
||||
|
||||
Requirements::restore();
|
||||
|
||||
return self::mailer()->sendHTML($to, $this->from, $subject, $this->body, $this->attachments, $headers,
|
||||
return self::mailer()->sendHTML($to, $from, $subject, $this->body, $this->attachments, $headers,
|
||||
$this->plaintext_body);
|
||||
}
|
||||
|
||||
@ -511,7 +566,7 @@ class Email extends ViewableData {
|
||||
public static function send_all_emails_to($emailAddress) {
|
||||
self::$send_all_emails_to = $emailAddress;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* CC every email generated by the Email class to the given address.
|
||||
* It won't affect the original delivery in the same way that send_all_emails_to does. It just adds a CC header
|
||||
|
491
email/Mailer.php
491
email/Mailer.php
@ -27,7 +27,7 @@ class Mailer {
|
||||
if ($customheaders && is_array($customheaders) == false) {
|
||||
echo "htmlEmail($to, $from, $subject, ...) could not send mail: improper \$customheaders passed:<BR>";
|
||||
dieprintr($customheaders);
|
||||
}
|
||||
}
|
||||
|
||||
// If the subject line contains extended characters, we must encode it
|
||||
$subject = Convert::xml2raw($subject);
|
||||
@ -53,9 +53,8 @@ class Mailer {
|
||||
$messageParts[] = $this->encodeFileForEmail($file['tmp_name'], $file['name']);
|
||||
} else {
|
||||
$messageParts[] = $this->encodeFileForEmail($file);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// We further wrap all of this into another multipart block
|
||||
list($fullBody, $headers) = $this->encodeMultipart($messageParts, "multipart/mixed");
|
||||
@ -105,63 +104,60 @@ class Mailer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email as a both HTML and plaintext
|
||||
* Sends an email as a both HTML and plaintext
|
||||
*
|
||||
* $attachedFiles should be an array of file names
|
||||
* - if you pass the entire $_FILES entry, the user-uploaded filename will be preserved
|
||||
* use $plainContent to override default plain-content generation
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
* $attachedFiles should be an array of file names
|
||||
* - if you pass the entire $_FILES entry, the user-uploaded filename will be preserved
|
||||
* use $plainContent to override default plain-content generation
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function sendHTML($to, $from, $subject, $htmlContent, $attachedFiles = false, $customheaders = false,
|
||||
$plainContent = false) {
|
||||
|
||||
if ($customheaders && is_array($customheaders) == false) {
|
||||
echo "htmlEmail($to, $from, $subject, ...) could not send mail: improper \$customheaders passed:<BR>";
|
||||
dieprintr($customheaders);
|
||||
}
|
||||
if ($customheaders && is_array($customheaders) == false) {
|
||||
echo "htmlEmail($to, $from, $subject, ...) could not send mail: improper \$customheaders passed:<BR>";
|
||||
dieprintr($customheaders);
|
||||
}
|
||||
|
||||
|
||||
$bodyIsUnicode = (strpos($htmlContent,"&#") !== false);
|
||||
$plainEncoding = "";
|
||||
|
||||
// We generate plaintext content by default, but you can pass custom stuff
|
||||
$plainEncoding = '';
|
||||
if(!$plainContent) {
|
||||
$plainContent = Convert::xml2raw($htmlContent);
|
||||
if(isset($bodyIsUnicode) && $bodyIsUnicode) $plainEncoding = "base64";
|
||||
}
|
||||
$bodyIsUnicode = (strpos($htmlContent,"&#") !== false);
|
||||
$plainEncoding = "";
|
||||
|
||||
// We generate plaintext content by default, but you can pass custom stuff
|
||||
$plainEncoding = '';
|
||||
if(!$plainContent) {
|
||||
$plainContent = Convert::xml2raw($htmlContent);
|
||||
if(isset($bodyIsUnicode) && $bodyIsUnicode) $plainEncoding = "base64";
|
||||
}
|
||||
|
||||
// If the subject line contains extended characters, we must encode the
|
||||
$subject = Convert::xml2raw($subject);
|
||||
$subject = "=?UTF-8?B?" . base64_encode($subject) . "?=";
|
||||
|
||||
// If the subject line contains extended characters, we must encode the
|
||||
$subject = Convert::xml2raw($subject);
|
||||
$subject = "=?UTF-8?B?" . base64_encode($subject) . "?=";
|
||||
|
||||
// Make the plain text part
|
||||
$headers["Content-Type"] = "text/plain; charset=utf-8";
|
||||
$headers["Content-Transfer-Encoding"] = $plainEncoding ? $plainEncoding : "quoted-printable";
|
||||
// Make the plain text part
|
||||
$headers["Content-Type"] = "text/plain; charset=utf-8";
|
||||
$headers["Content-Transfer-Encoding"] = $plainEncoding ? $plainEncoding : "quoted-printable";
|
||||
|
||||
$plainPart = $this->processHeaders($headers, ($plainEncoding == "base64")
|
||||
? chunk_split(base64_encode($plainContent),60)
|
||||
: wordwrap($this->QuotedPrintable_encode($plainContent),75));
|
||||
|
||||
// Make the HTML part
|
||||
$headers["Content-Type"] = "text/html; charset=utf-8";
|
||||
|
||||
|
||||
// Add basic wrapper tags if the body tag hasn't been given
|
||||
if(stripos($htmlContent, '<body') === false) {
|
||||
$htmlContent =
|
||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n" .
|
||||
"<HTML><HEAD>\n" .
|
||||
"<META http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n" .
|
||||
"<STYLE type=\"text/css\"></STYLE>\n\n".
|
||||
"</HEAD>\n" .
|
||||
"<BODY bgColor=\"#ffffff\">\n" .
|
||||
$htmlContent .
|
||||
"\n</BODY>\n" .
|
||||
"</HTML>";
|
||||
}
|
||||
// Make the HTML part
|
||||
$headers["Content-Type"] = "text/html; charset=utf-8";
|
||||
|
||||
// Add basic wrapper tags if the body tag hasn't been given
|
||||
if(stripos($htmlContent, '<body') === false) {
|
||||
$htmlContent =
|
||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n" .
|
||||
"<HTML><HEAD>\n" .
|
||||
"<META http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n" .
|
||||
"<STYLE type=\"text/css\"></STYLE>\n\n".
|
||||
"</HEAD>\n" .
|
||||
"<BODY bgColor=\"#ffffff\">\n" .
|
||||
$htmlContent .
|
||||
"\n</BODY>\n" .
|
||||
"</HTML>";
|
||||
}
|
||||
|
||||
$headers["Content-Transfer-Encoding"] = "quoted-printable";
|
||||
$htmlPart = $this->processHeaders($headers, wordwrap($this->QuotedPrintable_encode($htmlContent),75));
|
||||
@ -171,202 +167,205 @@ class Mailer {
|
||||
"multipart/alternative"
|
||||
);
|
||||
|
||||
// Messages with attachments are handled differently
|
||||
if($attachedFiles && is_array($attachedFiles)) {
|
||||
|
||||
// The first part is the message itself
|
||||
$fullMessage = $this->processHeaders($messageHeaders, $messageBody);
|
||||
$messageParts = array($fullMessage);
|
||||
|
||||
// Include any specified attachments as additional parts
|
||||
foreach($attachedFiles as $file) {
|
||||
if(isset($file['tmp_name']) && isset($file['name'])) {
|
||||
$messageParts[] = $this->encodeFileForEmail($file['tmp_name'], $file['name']);
|
||||
} else {
|
||||
$messageParts[] = $this->encodeFileForEmail($file);
|
||||
}
|
||||
}
|
||||
// Messages with attachments are handled differently
|
||||
if($attachedFiles && is_array($attachedFiles)) {
|
||||
|
||||
// We further wrap all of this into another multipart block
|
||||
list($fullBody, $headers) = $this->encodeMultipart($messageParts, "multipart/mixed");
|
||||
// The first part is the message itself
|
||||
$fullMessage = $this->processHeaders($messageHeaders, $messageBody);
|
||||
$messageParts = array($fullMessage);
|
||||
|
||||
// Messages without attachments do not require such treatment
|
||||
} else {
|
||||
$headers = $messageHeaders;
|
||||
$fullBody = $messageBody;
|
||||
}
|
||||
// Include any specified attachments as additional parts
|
||||
foreach($attachedFiles as $file) {
|
||||
if(isset($file['tmp_name']) && isset($file['name'])) {
|
||||
$messageParts[] = $this->encodeFileForEmail($file['tmp_name'], $file['name']);
|
||||
} else {
|
||||
$messageParts[] = $this->encodeFileForEmail($file);
|
||||
}
|
||||
}
|
||||
|
||||
// We further wrap all of this into another multipart block
|
||||
list($fullBody, $headers) = $this->encodeMultipart($messageParts, "multipart/mixed");
|
||||
|
||||
// Email headers
|
||||
// Messages without attachments do not require such treatment
|
||||
} else {
|
||||
$headers = $messageHeaders;
|
||||
$fullBody = $messageBody;
|
||||
}
|
||||
|
||||
// Email headers
|
||||
$headers["From"] = $this->validEmailAddr($from);
|
||||
|
||||
// Messages with the X-SilverStripeMessageID header can be tracked
|
||||
if(isset($customheaders["X-SilverStripeMessageID"]) && defined('BOUNCE_EMAIL')) {
|
||||
$bounceAddress = BOUNCE_EMAIL;
|
||||
} else {
|
||||
$bounceAddress = $from;
|
||||
}
|
||||
// Messages with the X-SilverStripeMessageID header can be tracked
|
||||
if(isset($customheaders["X-SilverStripeMessageID"]) && defined('BOUNCE_EMAIL')) {
|
||||
$bounceAddress = BOUNCE_EMAIL;
|
||||
} else {
|
||||
$bounceAddress = $from;
|
||||
}
|
||||
|
||||
// Strip the human name from the bounce address
|
||||
if(preg_match('/^([^<>]*)<([^<>]+)> *$/', $bounceAddress, $parts)) $bounceAddress = $parts[2];
|
||||
// Strip the human name from the bounce address
|
||||
if(preg_match('/^([^<>]*)<([^<>]+)> *$/', $bounceAddress, $parts)) $bounceAddress = $parts[2];
|
||||
|
||||
// $headers["Sender"] = $from;
|
||||
$headers["X-Mailer"] = X_MAILER;
|
||||
if (!isset($customheaders["X-Priority"])) $headers["X-Priority"] = 3;
|
||||
|
||||
$headers = array_merge((array)$headers, (array)$customheaders);
|
||||
|
||||
// the carbon copy header has to be 'Cc', not 'CC' or 'cc' -- ensure this.
|
||||
if (isset($headers['CC'])) { $headers['Cc'] = $headers['CC']; unset($headers['CC']); }
|
||||
if (isset($headers['cc'])) { $headers['Cc'] = $headers['cc']; unset($headers['cc']); }
|
||||
|
||||
// the carbon copy header has to be 'Bcc', not 'BCC' or 'bcc' -- ensure this.
|
||||
if (isset($headers['BCC'])) {$headers['Bcc']=$headers['BCC']; unset($headers['BCC']); }
|
||||
if (isset($headers['bcc'])) {$headers['Bcc']=$headers['bcc']; unset($headers['bcc']); }
|
||||
// $headers["Sender"] = $from;
|
||||
$headers["X-Mailer"] = X_MAILER;
|
||||
if (!isset($customheaders["X-Priority"])) $headers["X-Priority"] = 3;
|
||||
|
||||
|
||||
// Send the email
|
||||
$headers = array_merge((array)$headers, (array)$customheaders);
|
||||
|
||||
// the carbon copy header has to be 'Cc', not 'CC' or 'cc' -- ensure this.
|
||||
if (isset($headers['CC'])) { $headers['Cc'] = $headers['CC']; unset($headers['CC']); }
|
||||
if (isset($headers['cc'])) { $headers['Cc'] = $headers['cc']; unset($headers['cc']); }
|
||||
|
||||
// the carbon copy header has to be 'Bcc', not 'BCC' or 'bcc' -- ensure this.
|
||||
if (isset($headers['BCC'])) {$headers['Bcc']=$headers['BCC']; unset($headers['BCC']); }
|
||||
if (isset($headers['bcc'])) {$headers['Bcc']=$headers['bcc']; unset($headers['bcc']); }
|
||||
|
||||
// Send the email
|
||||
$headers = $this->processHeaders($headers);
|
||||
$to = $this->validEmailAddr($to);
|
||||
|
||||
// Try it without the -f option if it fails
|
||||
if(!($result = @mail($to, $subject, $fullBody, $headers, escapeshellarg("-f$bounceAddress")))) {
|
||||
$result = mail($to, $subject, $fullBody, $headers);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Make visibility protected in 3.2
|
||||
*/
|
||||
function encodeMultipart($parts, $contentType, $headers = false) {
|
||||
$separator = "----=_NextPart_" . preg_replace('/[^0-9]/', '', rand() * 10000000000);
|
||||
|
||||
$headers["MIME-Version"] = "1.0";
|
||||
$headers["Content-Type"] = "$contentType; boundary=\"$separator\"";
|
||||
$headers["Content-Transfer-Encoding"] = "7bit";
|
||||
|
||||
if($contentType == "multipart/alternative") {
|
||||
// $baseMessage = "This is an encoded HTML message. There are two parts: a plain text and an HTML message,
|
||||
// open whatever suits you better.";
|
||||
$baseMessage = "\nThis is a multi-part message in MIME format.";
|
||||
} else {
|
||||
// $baseMessage = "This is a message containing attachments. The e-mail body is contained in the first
|
||||
// attachment";
|
||||
$baseMessage = "\nThis is a multi-part message in MIME format.";
|
||||
|
||||
// Try it without the -f option if it fails
|
||||
if(!($result = @mail($to, $subject, $fullBody, $headers, escapeshellarg("-f$bounceAddress")))) {
|
||||
$result = mail($to, $subject, $fullBody, $headers);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Make visibility protected in 3.2
|
||||
*/
|
||||
function encodeMultipart($parts, $contentType, $headers = false) {
|
||||
$separator = "----=_NextPart_" . preg_replace('/[^0-9]/', '', rand() * 10000000000);
|
||||
|
||||
$separator = "\n--$separator\n";
|
||||
$body = "$baseMessage\n" .
|
||||
$separator . implode("\n".$separator, $parts) . "\n" . trim($separator) . "--";
|
||||
$headers["MIME-Version"] = "1.0";
|
||||
$headers["Content-Type"] = "$contentType; boundary=\"$separator\"";
|
||||
$headers["Content-Transfer-Encoding"] = "7bit";
|
||||
|
||||
return array($body, $headers);
|
||||
}
|
||||
if($contentType == "multipart/alternative") {
|
||||
// $baseMessage = "This is an encoded HTML message. There are two parts: a plain text and an HTML message,
|
||||
// open whatever suits you better.";
|
||||
$baseMessage = "\nThis is a multi-part message in MIME format.";
|
||||
} else {
|
||||
// $baseMessage = "This is a message containing attachments. The e-mail body is contained in the first
|
||||
// attachment";
|
||||
$baseMessage = "\nThis is a multi-part message in MIME format.";
|
||||
}
|
||||
|
||||
$separator = "\n--$separator\n";
|
||||
$body = "$baseMessage\n" .
|
||||
$separator . implode("\n".$separator, $parts) . "\n" . trim($separator) . "--";
|
||||
|
||||
return array($body, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Make visibility protected in 3.2
|
||||
*/
|
||||
function processHeaders($headers, $body = false) {
|
||||
$res = '';
|
||||
if(is_array($headers)) {
|
||||
while(list($k, $v) = each($headers)) {
|
||||
$res .= "$k: $v\n";
|
||||
}
|
||||
}
|
||||
if($body) $res .= "\n$body";
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Make visibility protected in 3.2
|
||||
*/
|
||||
function processHeaders($headers, $body = false) {
|
||||
$res = '';
|
||||
if(is_array($headers)) while(list($k, $v) = each($headers))
|
||||
$res .= "$k: $v\n";
|
||||
if($body) $res .= "\n$body";
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the contents of a file for emailing, including headers
|
||||
*
|
||||
* $file can be an array, in which case it expects these members:
|
||||
* 'filename' - the filename of the file
|
||||
* 'contents' - the raw binary contents of the file as a string
|
||||
* and can optionally include these members:
|
||||
* 'mimetype' - the mimetype of the file (calculated from filename if missing)
|
||||
* 'contentLocation' - the 'Content-Location' header value for the file
|
||||
*
|
||||
* $file can also be a string, in which case it is assumed to be the filename
|
||||
*
|
||||
* h5. contentLocation
|
||||
*
|
||||
* Encode the contents of a file for emailing, including headers
|
||||
*
|
||||
* $file can be an array, in which case it expects these members:
|
||||
* 'filename' - the filename of the file
|
||||
* 'contents' - the raw binary contents of the file as a string
|
||||
* and can optionally include these members:
|
||||
* 'mimetype' - the mimetype of the file (calculated from filename if missing)
|
||||
* 'contentLocation' - the 'Content-Location' header value for the file
|
||||
*
|
||||
* $file can also be a string, in which case it is assumed to be the filename
|
||||
*
|
||||
* h5. contentLocation
|
||||
*
|
||||
* Content Location is one of the two methods allowed for embedding images into an html email.
|
||||
* It's also the simplest, and best supported.
|
||||
*
|
||||
* Assume we have an email with this in the body:
|
||||
*
|
||||
* <img src="http://example.com/image.gif" />
|
||||
*
|
||||
*
|
||||
* Assume we have an email with this in the body:
|
||||
*
|
||||
* <img src="http://example.com/image.gif" />
|
||||
*
|
||||
* To display the image, an email viewer would have to download the image from the web every time
|
||||
* it is displayed. Due to privacy issues, most viewers will not display any images unless
|
||||
* the user clicks 'Show images in this email'. Not optimal.
|
||||
*
|
||||
*
|
||||
* However, we can also include a copy of this image as an attached file in the email.
|
||||
* By giving it a contentLocation of "http://example.com/image.gif" most email viewers
|
||||
* will use this attached copy instead of downloading it. Better,
|
||||
* most viewers will show it without a 'Show images in this email' conformation.
|
||||
*
|
||||
* Here is an example of passing this information through Email.php:
|
||||
*
|
||||
* $email = new Email();
|
||||
* $email->attachments[] = array(
|
||||
* 'filename' => BASE_PATH . "/themes/mytheme/images/header.gif",
|
||||
* 'contents' => file_get_contents(BASE_PATH . "/themes/mytheme/images/header.gif"),
|
||||
* 'mimetype' => 'image/gif',
|
||||
* 'contentLocation' => Director::absoluteBaseURL() . "/themes/mytheme/images/header.gif"
|
||||
* );
|
||||
*
|
||||
* most viewers will show it without a 'Show images in this email' conformation.
|
||||
*
|
||||
* Here is an example of passing this information through Email.php:
|
||||
*
|
||||
* $email = new Email();
|
||||
* $email->attachments[] = array(
|
||||
* 'filename' => BASE_PATH . "/themes/mytheme/images/header.gif",
|
||||
* 'contents' => file_get_contents(BASE_PATH . "/themes/mytheme/images/header.gif"),
|
||||
* 'mimetype' => 'image/gif',
|
||||
* 'contentLocation' => Director::absoluteBaseURL() . "/themes/mytheme/images/header.gif"
|
||||
* );
|
||||
*
|
||||
* @todo Make visibility protected in 3.2
|
||||
*/
|
||||
function encodeFileForEmail($file, $destFileName = false, $disposition = NULL, $extraHeaders = "") {
|
||||
if(!$file) {
|
||||
user_error("encodeFileForEmail: not passed a filename and/or data", E_USER_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_string($file)) {
|
||||
$file = array('filename' => $file);
|
||||
$fh = fopen($file['filename'], "rb");
|
||||
if ($fh) {
|
||||
while(!feof($fh)) $file['contents'] .= fread($fh, 10000);
|
||||
fclose($fh);
|
||||
*/
|
||||
function encodeFileForEmail($file, $destFileName = false, $disposition = NULL, $extraHeaders = "") {
|
||||
if(!$file) {
|
||||
user_error("encodeFileForEmail: not passed a filename and/or data", E_USER_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_string($file)) {
|
||||
$file = array('filename' => $file);
|
||||
$fh = fopen($file['filename'], "rb");
|
||||
if ($fh) {
|
||||
$file['contents'] = "";
|
||||
while(!feof($fh)) $file['contents'] .= fread($fh, 10000);
|
||||
fclose($fh);
|
||||
}
|
||||
}
|
||||
|
||||
// Build headers, including content type
|
||||
if(!$destFileName) $base = basename($file['filename']);
|
||||
else $base = $destFileName;
|
||||
|
||||
$mimeType = !empty($file['mimetype']) ? $file['mimetype'] : HTTP::get_mime_type($file['filename']);
|
||||
if(!$mimeType) $mimeType = "application/unknown";
|
||||
if (empty($disposition)) $disposition = isset($file['contentLocation']) ? 'inline' : 'attachment';
|
||||
|
||||
// Encode for emailing
|
||||
if (substr($mimeType, 0, 4) != 'text') {
|
||||
$encoding = "base64";
|
||||
$file['contents'] = chunk_split(base64_encode($file['contents']));
|
||||
} else {
|
||||
// This mime type is needed, otherwise some clients will show it as an inline attachment
|
||||
$mimeType = 'application/octet-stream';
|
||||
$encoding = "quoted-printable";
|
||||
$file['contents'] = $this->QuotedPrintable_encode($file['contents']);
|
||||
}
|
||||
|
||||
$headers = "Content-type: $mimeType;\n\tname=\"$base\"\n".
|
||||
"Content-Transfer-Encoding: $encoding\n".
|
||||
"Content-Disposition: $disposition;\n\tfilename=\"$base\"\n";
|
||||
|
||||
if ( isset($file['contentLocation']) ) $headers .= 'Content-Location: ' . $file['contentLocation'] . "\n" ;
|
||||
|
||||
$headers .= $extraHeaders . "\n";
|
||||
|
||||
// Return completed packet
|
||||
return $headers . $file['contents'];
|
||||
}
|
||||
|
||||
// Build headers, including content type
|
||||
if(!$destFileName) $base = basename($file['filename']);
|
||||
else $base = $destFileName;
|
||||
|
||||
$mimeType = $file['mimetype'] ? $file['mimetype'] : HTTP::get_mime_type($file['filename']);
|
||||
if(!$mimeType) $mimeType = "application/unknown";
|
||||
if (empty($disposition)) $disposition = isset($file['contentLocation']) ? 'inline' : 'attachment';
|
||||
|
||||
// Encode for emailing
|
||||
if (substr($file['mimetype'], 0, 4) != 'text') {
|
||||
$encoding = "base64";
|
||||
$file['contents'] = chunk_split(base64_encode($file['contents']));
|
||||
} else {
|
||||
// This mime type is needed, otherwise some clients will show it as an inline attachment
|
||||
$mimeType = 'application/octet-stream';
|
||||
$encoding = "quoted-printable";
|
||||
$file['contents'] = $this->QuotedPrintable_encode($file['contents']);
|
||||
}
|
||||
|
||||
$headers = "Content-type: $mimeType;\n\tname=\"$base\"\n".
|
||||
"Content-Transfer-Encoding: $encoding\n".
|
||||
"Content-Disposition: $disposition;\n\tfilename=\"$base\"\n";
|
||||
|
||||
if ( isset($file['contentLocation']) ) $headers .= 'Content-Location: ' . $file['contentLocation'] . "\n" ;
|
||||
|
||||
$headers .= $extraHeaders . "\n";
|
||||
|
||||
// Return completed packet
|
||||
return $headers . $file['contents'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Make visibility protected in 3.2
|
||||
*/
|
||||
function QuotedPrintable_encode($quotprint) {
|
||||
function QuotedPrintable_encode($quotprint) {
|
||||
$quotprint = (string)str_replace('\r\n',chr(13).chr(10),$quotprint);
|
||||
$quotprint = (string)str_replace('\n', chr(13).chr(10),$quotprint);
|
||||
$quotprint = (string)preg_replace_callback("~([\x01-\x1F\x3D\x7F-\xFF])~", function($matches) {
|
||||
@ -378,25 +377,63 @@ function QuotedPrintable_encode($quotprint) {
|
||||
$quotprint = (string)str_replace('=0D',"\n",$quotprint);
|
||||
$quotprint = (string)str_replace('=0A',"\n",$quotprint);
|
||||
return (string) $quotprint;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Make visibility protected in 3.2
|
||||
*/
|
||||
function validEmailAddr($emailAddress) {
|
||||
$emailAddress = trim($emailAddress);
|
||||
$angBrack = strpos($emailAddress, '<');
|
||||
|
||||
if($angBrack === 0) {
|
||||
$emailAddress = substr($emailAddress, 1, strpos($emailAddress,'>')-1);
|
||||
function validEmailAddr($emailAddress) {
|
||||
$emailAddress = trim($emailAddress);
|
||||
$angBrack = strpos($emailAddress, '<');
|
||||
|
||||
if($angBrack === 0) {
|
||||
$emailAddress = substr($emailAddress, 1, strpos($emailAddress,'>')-1);
|
||||
|
||||
} else if($angBrack) {
|
||||
$emailAddress = str_replace('@', '', substr($emailAddress, 0, $angBrack))
|
||||
. substr($emailAddress, $angBrack);
|
||||
}
|
||||
|
||||
return $emailAddress;
|
||||
}
|
||||
|
||||
/*
|
||||
* Return a multipart/related e-mail chunk for the given HTML message and its linked images
|
||||
* Decodes absolute URLs, accessing the appropriate local images
|
||||
*/
|
||||
function wrapImagesInline($htmlContent) {
|
||||
global $_INLINED_IMAGES;
|
||||
$_INLINED_IMAGES = null;
|
||||
|
||||
$replacedContent = imageRewriter($htmlContent, 'wrapImagesInline_rewriter($URL)');
|
||||
|
||||
// Make the HTML part
|
||||
$headers["Content-Type"] = "text/html; charset=utf-8";
|
||||
$headers["Content-Transfer-Encoding"] = "quoted-printable";
|
||||
$multiparts[] = processHeaders($headers, QuotedPrintable_encode($replacedContent));
|
||||
|
||||
// Make all the image parts
|
||||
global $_INLINED_IMAGES;
|
||||
foreach($_INLINED_IMAGES as $url => $cid) {
|
||||
$multiparts[] = encodeFileForEmail($url, false, "inline", "Content-ID: <$cid>\n");
|
||||
}
|
||||
|
||||
// Merge together in a multipart
|
||||
list($body, $headers) = encodeMultipart($multiparts, "multipart/related");
|
||||
return processHeaders($headers, $body);
|
||||
}
|
||||
|
||||
function wrapImagesInline_rewriter($url) {
|
||||
$url = relativiseURL($url);
|
||||
|
||||
global $_INLINED_IMAGES;
|
||||
if(!$_INLINED_IMAGES[$url]) {
|
||||
$identifier = "automatedmessage." . rand(1000,1000000000) . "@silverstripe.com";
|
||||
$_INLINED_IMAGES[$url] = $identifier;
|
||||
}
|
||||
return "cid:" . $_INLINED_IMAGES[$url];
|
||||
|
||||
} else if($angBrack) {
|
||||
$emailAddress = str_replace('@', '', substr($emailAddress, 0, $angBrack))
|
||||
.substr($emailAddress, $angBrack);
|
||||
}
|
||||
|
||||
return $emailAddress;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -928,7 +928,7 @@ class File extends DataObject {
|
||||
if(!is_array($exts)) $exts = array($exts);
|
||||
|
||||
foreach($exts as $ext) {
|
||||
if(is_subclass_of($ext, 'File')) {
|
||||
if(!is_subclass_of($ext, 'File')) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('Class "%s" (for extension "%s") is not a valid subclass of File', $class, $ext)
|
||||
);
|
||||
|
@ -58,8 +58,10 @@ class DatetimeField extends FormField {
|
||||
public function __construct($name, $title = null, $value = ""){
|
||||
$this->config = self::$default_config;
|
||||
|
||||
$this->dateField = DateField::create($name . '[date]', false);
|
||||
$this->timeField = TimeField::create($name . '[time]', false);
|
||||
$this->dateField = DateField::create($name . '[date]', false)
|
||||
->addExtraClass('fieldgroup-field');
|
||||
$this->timeField = TimeField::create($name . '[time]', false)
|
||||
->addExtraClass('fieldgroup-field');
|
||||
$this->timezoneField = new HiddenField($this->getName() . '[timezone]');
|
||||
|
||||
parent::__construct($name, $title, $value);
|
||||
@ -80,6 +82,7 @@ class DatetimeField extends FormField {
|
||||
'datetimeorder' => $this->getConfig('datetimeorder'),
|
||||
);
|
||||
$config = array_filter($config);
|
||||
$this->addExtraClass('fieldgroup');
|
||||
$this->addExtraClass(Convert::raw2json($config));
|
||||
|
||||
return parent::FieldHolder($properties);
|
||||
@ -212,6 +215,24 @@ class DatetimeField extends FormField {
|
||||
public function getDateField() {
|
||||
return $this->dateField;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormField
|
||||
*/
|
||||
public function setDateField($field) {
|
||||
$expected = $this->getName() . '[date]';
|
||||
if($field->getName() != $expected) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Wrong name format for date field: "%s" (expected "%s")',
|
||||
$field->getName(),
|
||||
$expected
|
||||
));
|
||||
}
|
||||
|
||||
$field->setForm($this->getForm());
|
||||
$this->dateField = $field;
|
||||
$this->setValue($this->value); // update value
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TimeField
|
||||
@ -219,6 +240,24 @@ class DatetimeField extends FormField {
|
||||
public function getTimeField() {
|
||||
return $this->timeField;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormField
|
||||
*/
|
||||
public function setTimeField($field) {
|
||||
$expected = $this->getName() . '[time]';
|
||||
if($field->getName() != $expected) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Wrong name format for time field: "%s" (expected "%s")',
|
||||
$field->getName(),
|
||||
$expected
|
||||
));
|
||||
}
|
||||
|
||||
$field->setForm($this->getForm());
|
||||
$this->timeField = $field;
|
||||
$this->setValue($this->value); // update value
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormField
|
||||
@ -236,15 +275,6 @@ class DatetimeField extends FormField {
|
||||
public function getLocale() {
|
||||
return $this->dateField->getLocale();
|
||||
}
|
||||
|
||||
public function setDescription($description) {
|
||||
parent::setDescription($description);
|
||||
|
||||
$this->dateField->setDescription($description);
|
||||
$this->timeField->setDescription($description);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Use {@link getDateField()} and {@link getTimeField()}
|
||||
|
@ -250,7 +250,9 @@ class Form extends RequestHandler {
|
||||
// Protection against CSRF attacks
|
||||
$token = $this->getSecurityToken();
|
||||
if(!$token->checkRequest($request)) {
|
||||
$this->httpError(400, "Sorry, your session has timed out.");
|
||||
$this->httpError(400, _t("Form.CSRF_FAILED_MESSAGE",
|
||||
"There seems to have been a technical problem. Please click the back button,"
|
||||
. " refresh your browser, and try again."));
|
||||
}
|
||||
|
||||
// Determine the action button clicked
|
||||
@ -655,7 +657,9 @@ class Form extends RequestHandler {
|
||||
$needsCacheDisabled = false;
|
||||
if ($this->getSecurityToken()->isEnabled()) $needsCacheDisabled = true;
|
||||
if ($this->FormMethod() != 'get') $needsCacheDisabled = true;
|
||||
if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) $needsCacheDisabled = true;
|
||||
if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) {
|
||||
$needsCacheDisabled = true;
|
||||
}
|
||||
|
||||
// If we need to disable cache, do it
|
||||
if ($needsCacheDisabled) HTTP::set_cache_age(0);
|
||||
|
@ -37,21 +37,27 @@ class MemberDatetimeOptionsetField extends OptionsetField {
|
||||
$value = ($this->value && !array_key_exists($this->value, $this->source)) ? $this->value : null;
|
||||
$checked = ($value) ? " checked=\"checked\"" : '';
|
||||
$options .= "<li class=\"valCustom\">"
|
||||
. sprintf("<input id=\"%s_custom\" name=\"%s\" type=\"radio\" value=\"__custom__\" class=\"radio\" %s />",
|
||||
$itemID, $this->name, $checked)
|
||||
. sprintf('<label for="%s_custom">%s:</label>',
|
||||
$itemID, _t('MemberDatetimeOptionsetField.Custom', 'Custom'))
|
||||
. sprintf(
|
||||
"<input id=\"%s_custom\" name=\"%s\" type=\"radio\" value=\"__custom__\" class=\"radio\" %s />",
|
||||
$itemID, $this->name,
|
||||
$checked
|
||||
)
|
||||
. sprintf(
|
||||
'<label for="%s_custom">%s:</label>',
|
||||
$itemID, _t('MemberDatetimeOptionsetField.Custom', 'Custom')
|
||||
)
|
||||
. sprintf(
|
||||
"<input class=\"customFormat cms-help cms-help-tooltip\" name=\"%s_custom\" value=\"%s\" />\n",
|
||||
$this->name,
|
||||
$value
|
||||
)
|
||||
. sprintf("<input type=\"hidden\" class=\"formatValidationURL\" value=\"%s\" />",
|
||||
$this->Link() . '/validate');
|
||||
$this->name, Convert::raw2xml($value)
|
||||
)
|
||||
. sprintf(
|
||||
"<input type=\"hidden\" class=\"formatValidationURL\" value=\"%s\" />",
|
||||
$this->Link() . '/validate'
|
||||
);
|
||||
$options .= ($value) ? sprintf(
|
||||
'<span class="preview">(%s: "%s")</span>',
|
||||
_t('MemberDatetimeOptionsetField.Preview', 'Preview'),
|
||||
Zend_Date::now()->toString($value)
|
||||
Convert::raw2xml(Zend_Date::now()->toString($value))
|
||||
) : '';
|
||||
|
||||
$id = $this->id();
|
||||
|
@ -35,7 +35,7 @@ class RequiredFields extends Validator {
|
||||
* Clears all the validation from this object.
|
||||
*/
|
||||
public function removeValidation(){
|
||||
$this->required = null;
|
||||
$this->required = array();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,7 +27,7 @@ class TimeField extends TextField {
|
||||
* @var array
|
||||
*/
|
||||
static $default_config = array(
|
||||
'timeformat' => 'HH:mm:ss',
|
||||
'timeformat' => null,
|
||||
'use_strtotime' => true,
|
||||
'datavalueformat' => 'HH:mm:ss'
|
||||
);
|
||||
|
@ -421,6 +421,7 @@ class TreeDropdownField_Readonly extends TreeDropdownField {
|
||||
$field = new LookupField($this->name, $this->title, $source);
|
||||
$field->setValue($this->value);
|
||||
$field->setForm($this->form);
|
||||
$field->dontEscape = true;
|
||||
return $field->Field();
|
||||
}
|
||||
}
|
||||
|
@ -612,7 +612,7 @@ class GridField extends FormField {
|
||||
$stateChange = Session::get($id);
|
||||
$actionName = $stateChange['actionName'];
|
||||
$args = isset($stateChange['args']) ? $stateChange['args'] : array();
|
||||
$html = $this->handleAction($actionName, $args, $data);
|
||||
$html = $this->handleAlterAction($actionName, $args, $data);
|
||||
// A field can optionally return its own HTML
|
||||
if($html) return $html;
|
||||
}
|
||||
@ -642,7 +642,7 @@ class GridField extends FormField {
|
||||
* @return type
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function handleAction($actionName, $args, $data) {
|
||||
public function handleAlterAction($actionName, $args, $data) {
|
||||
$actionName = strtolower($actionName);
|
||||
foreach($this->getComponents() as $component) {
|
||||
if(!($component instanceof GridField_ActionProvider)) {
|
||||
|
@ -369,11 +369,11 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
|
||||
$form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
|
||||
|
||||
if($this->record->ID && !$canEdit) {
|
||||
// Restrict editing of existing records
|
||||
$form->makeReadonly();
|
||||
// Restrict editing of existing records
|
||||
$form->makeReadonly();
|
||||
} elseif(!$this->record->ID && !$canCreate) {
|
||||
// Restrict creation of new records
|
||||
$form->makeReadonly();
|
||||
// Restrict creation of new records
|
||||
$form->makeReadonly();
|
||||
}
|
||||
|
||||
// Load many_many extraData for record.
|
||||
|
BIN
images/drive-upload-large.png
Normal file
BIN
images/drive-upload-large.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
@ -135,7 +135,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
$('.ss-gridfield .action.gridfield-button-delete').entwine({
|
||||
$('.ss-gridfield .action.gridfield-button-delete, .cms-content-actions .ss-ui-action-destructive .ui-button-text').entwine({
|
||||
onclick: function(e){
|
||||
if(!confirm(ss.i18n._t('TABLEFIELD.DELETECONFIRMMESSAGE'))) {
|
||||
e.preventDefault();
|
||||
|
@ -290,6 +290,16 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
this._super();
|
||||
},
|
||||
|
||||
/**
|
||||
* Make sure the editor has flushed all it's buffers before the form is submitted.
|
||||
*/
|
||||
'from .cms-edit-form': {
|
||||
onbeforesubmitform: function(e) {
|
||||
this.getEditor().save();
|
||||
this._super();
|
||||
}
|
||||
},
|
||||
|
||||
oneditorinit: function() {
|
||||
// Delayed show because TinyMCE calls hide() via setTimeout on removing an element,
|
||||
// which is called in quick succession with adding a new editor after ajax loading new markup
|
||||
@ -790,8 +800,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
|
||||
// TODO Depends on managed mime type
|
||||
if(node.is('img')) {
|
||||
this.showFileView(node.data('url') || node.attr('src')).complete(function() {
|
||||
$(this).updateFromNode(node);
|
||||
this.showFileView(node.data('url') || node.attr('src')).done(function(filefield) {
|
||||
filefield.updateFromNode(node);
|
||||
self.toggleCloseButton();
|
||||
self.redraw();
|
||||
});
|
||||
@ -834,25 +844,29 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
getFileView: function(idOrUrl) {
|
||||
return this.find('.ss-htmleditorfield-file[data-id=' + idOrUrl + ']');
|
||||
},
|
||||
showFileView: function(idOrUrl, successCallback) {
|
||||
var self = this, params = (Number(idOrUrl) == idOrUrl) ? {ID: idOrUrl} : {FileURL: idOrUrl},
|
||||
item = $('<div class="ss-htmleditorfield-file" />');
|
||||
showFileView: function(idOrUrl) {
|
||||
var self = this, params = (Number(idOrUrl) == idOrUrl) ? {ID: idOrUrl} : {FileURL: idOrUrl};
|
||||
|
||||
item.addClass('loading');
|
||||
var item = $('<div class="ss-htmleditorfield-file loading" />');
|
||||
this.find('.content-edit').append(item);
|
||||
return $.ajax({
|
||||
// url: this.data('urlViewfile') + '?ID=' + id,
|
||||
|
||||
var dfr = $.Deferred();
|
||||
|
||||
$.ajax({
|
||||
url: $.path.addSearchParams(this.attr('action').replace(/MediaForm/, 'viewfile'), params),
|
||||
success: function(html, status, xhr) {
|
||||
var newItem = $(html);
|
||||
var newItem = $(html).filter('.ss-htmleditorfield-file');
|
||||
item.replaceWith(newItem);
|
||||
self.redraw();
|
||||
if(successCallback) successCallback.call(newItem, html, status, xhr);
|
||||
dfr.resolve(newItem);
|
||||
},
|
||||
error: function() {
|
||||
item.remove();
|
||||
dfr.reject();
|
||||
}
|
||||
});
|
||||
|
||||
return dfr.promise();
|
||||
}
|
||||
});
|
||||
|
||||
@ -941,7 +955,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
|
||||
if (urlField.validate()) {
|
||||
container.addClass('loading');
|
||||
form.showFileView('http://' + urlField.val()).complete(function() {
|
||||
form.showFileView('http://' + urlField.val()).done(function() {
|
||||
container.removeClass('loading');
|
||||
});
|
||||
form.redraw();
|
||||
@ -1043,44 +1057,69 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
|
||||
};
|
||||
},
|
||||
getHTML: function() {
|
||||
var el,
|
||||
attrs = this.getAttributes(),
|
||||
extraData = this.getExtraData(),
|
||||
// imgEl = $('<img id="_ss_tmp_img" />');
|
||||
imgEl = $('<img />').attr(attrs);
|
||||
|
||||
if(extraData.CaptionText) {
|
||||
el = $('<div style="width: ' + attrs['width'] + 'px;" class="captionImage ' + attrs['class'] + '"><p class="caption">' + extraData.CaptionText + '</p></div>').prepend(imgEl);
|
||||
} else {
|
||||
el = imgEl;
|
||||
}
|
||||
return $('<div />').append(el).html(); // Little hack to get outerHTML string
|
||||
/* NOP */
|
||||
},
|
||||
/**
|
||||
* Logic similar to TinyMCE 'advimage' plugin, insertAndClose() method.
|
||||
*/
|
||||
insertHTML: function(ed) {
|
||||
var form = this.closest('form'),
|
||||
node = form.getSelection(), captionNode = node.closest('.captionImage');
|
||||
var form = this.closest('form'), node = form.getSelection(), ed = form.getEditor();
|
||||
|
||||
if(node && node.is('img')) {
|
||||
// If the image exists, update it to avoid complications with inserting TinyMCE HTML content
|
||||
var attrs = this.getAttributes(), extraData = this.getExtraData();
|
||||
node.attr(attrs);
|
||||
// TODO Doesn't allow adding a caption to image after it was first added
|
||||
if(captionNode.length) {
|
||||
captionNode.find('.caption').text(extraData.CaptionText);
|
||||
captionNode.css({width: attrs.width, height: attrs.height}).attr('class', attrs['class']);
|
||||
// Get the attributes & extra data
|
||||
var attrs = this.getAttributes(), extraData = this.getExtraData();
|
||||
|
||||
// Find the element we are replacing - either the img, it's wrapper parent, or nothing (if creating)
|
||||
var replacee = (node && node.is('img')) ? node : null;
|
||||
if (replacee && replacee.parent().is('.captionImage')) replacee = replacee.parent();
|
||||
|
||||
// Find the img node - either the existing img or a new one, and update it
|
||||
var img = (node && node.is('img')) ? node : $('<img />');
|
||||
img.attr(attrs);
|
||||
|
||||
// Any existing figure or caption node
|
||||
var container = img.parent('.captionImage'), caption = container.find('.caption');
|
||||
|
||||
// If we've got caption text, we need a wrapping div.captionImage and sibling p.caption
|
||||
if (extraData.CaptionText) {
|
||||
if (!container.length) {
|
||||
container = $('<div></div>');
|
||||
}
|
||||
// Undo needs to be added manually as we're doing direct DOM changes
|
||||
ed.addUndo();
|
||||
} else {
|
||||
// Otherwise insert the whole HTML content
|
||||
ed.repaint();
|
||||
ed.insertContent(this.getHTML(), {skip_undo : 1});
|
||||
ed.addUndo(); // Not sure why undo is separate here, replicating TinyMCE logic
|
||||
|
||||
container.attr('class', 'captionImage '+attrs['class']).css('width', attrs.width);
|
||||
|
||||
if (!caption.length) {
|
||||
caption = $('<p class="caption"></p>').appendTo(container);
|
||||
}
|
||||
|
||||
caption.attr('class', 'caption '+attrs['class']).text(extraData.CaptionText);
|
||||
}
|
||||
// Otherwise forget they exist
|
||||
else {
|
||||
container = caption = null;
|
||||
}
|
||||
|
||||
// The element we are replacing the replacee with
|
||||
var replacer = container ? container : img;
|
||||
|
||||
// If we're replacing something, and it's not with itself, do so
|
||||
if (replacee && replacee.not(replacer).length) {
|
||||
replacee.replaceWith(replacer);
|
||||
}
|
||||
|
||||
// If we have a wrapper element, make sure the img is the first child - img might be the
|
||||
// replacee, and the wrapper the replacer, and we can't do this till after the replace has happened
|
||||
if (container) {
|
||||
container.prepend(img);
|
||||
}
|
||||
|
||||
// If we don't have a replacee, then we need to insert the whole HTML
|
||||
if (!replacee) {
|
||||
// Otherwise insert the whole HTML content
|
||||
ed.repaint();
|
||||
ed.insertContent($('<div />').append(replacer).html(), {skip_undo : 1});
|
||||
}
|
||||
|
||||
ed.addUndo();
|
||||
ed.repaint();
|
||||
},
|
||||
updateFromNode: function(node) {
|
||||
|
@ -159,7 +159,7 @@
|
||||
));
|
||||
|
||||
if (this.data('fileupload')._isXHRUpload({multipart: true})) {
|
||||
$('.ss-uploadfield-item-uploador').show();
|
||||
$('.ss-uploadfield-item-uploador').hide().show();
|
||||
dropZone.hide().show();
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,31 @@ if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
|
||||
'UNIQUEFIELD.CANNOTLEAVEEMPTY': 'Dit veld mag niet leeg blijven',
|
||||
'RESTRICTEDTEXTFIELD.CHARCANTBEUSED': "Het karakter '%s' mag niet gebruikt worden in dit veld",
|
||||
'UPDATEURL.CONFIRM': 'Wilt u de URL wijzigen naar:\n\n%s/\n\nKlik Ok om de URL te wijzigen, Klik Cancel om het'
|
||||
+ ' te laten zoals het is:\n\n%s'
|
||||
+ ' te laten zoals het is:\n\n%s',
|
||||
'UPDATEURL.CONFIRMURLCHANGED':'Het URL is veranderd naar \n"%s"',
|
||||
'FILEIFRAMEFIELD.DELETEFILE': 'Verwijder bestand',
|
||||
'FILEIFRAMEFIELD.UNATTACHFILE': 'Deselecteer bestand',
|
||||
'FILEIFRAMEFIELD.DELETEIMAGE': 'Verwijder afbeelding',
|
||||
'FILEIFRAMEFIELD.CONFIRMDELETE': 'Weet u zeker dat u dit bestand wilt verwijderen?',
|
||||
'LeftAndMain.IncompatBrowserWarning': 'Je huidige browser is niet compatible, gebruik één van deze browsers Internet Explorer 7+, Google Chrome 10+ or Mozilla Firefox 3.5+.',
|
||||
'GRIDFIELD.ERRORINTRANSACTION': 'Er is een fout opgetreden bij het ophalen van gegevens van de server\n Probeer later opnieuw.',
|
||||
'HtmlEditorField.SelectAnchor': 'Kies een anker',
|
||||
'UploadField.ConfirmDelete': 'Weet u zeker dat u dit bestand wilt verwijderen uit het websitebestand?',
|
||||
'UploadField.PHP_MAXFILESIZE': 'Bestandsgrootte is hoger dan upload_max_filesize (php.ini directive)',
|
||||
'UploadField.HTML_MAXFILESIZE': 'Bestandsgrootte is hoger danMAX_FILE_SIZE (HTML form directive)',
|
||||
'UploadField.ONLYPARTIALUPLOADED': 'Bestand is maar gedeeltelijk geupload',
|
||||
'UploadField.NOFILEUPLOADED': 'Geen bestand is geupload',
|
||||
'UploadField.NOTMPFOLDER': 'Mist een tijdelijke map',
|
||||
'UploadField.WRITEFAILED': 'Kan bestand niet naar schijf schrijven',
|
||||
'UploadField.STOPEDBYEXTENSION': 'Bestandsupload gestopt door extensie',
|
||||
'UploadField.TOOLARGE': 'Bestandsgrootte is te groot',
|
||||
'UploadField.TOOSMALL': 'Bestandsgrootte is te klein',
|
||||
'UploadField.INVALIDEXTENSION': 'Extensie is niet toegestaan',
|
||||
'UploadField.MAXNUMBEROFFILESSIMPLE': 'Maximaal aantal overschreven',
|
||||
'UploadField.UPLOADEDBYTES': 'Upload overschrijd bestandsgrootte',
|
||||
'UploadField.EMPTYRESULT': 'Leeg bestand geupload',
|
||||
'UploadField.LOADING': 'Laden ...',
|
||||
'UploadField.Editing': 'Bijwerken ...',
|
||||
'UploadField.Uploaded': 'Geupload'
|
||||
});
|
||||
}
|
||||
|
22
lang/nl.yml
22
lang/nl.yml
@ -19,7 +19,7 @@ nl:
|
||||
DROPAREA: 'Hierheen slepen'
|
||||
EDITALL: 'Alle bewerken'
|
||||
EDITANDORGANIZE: 'Bewerk en beheer'
|
||||
EDITINFO: 'Edit files'
|
||||
EDITINFO: 'Bewerk alle bestanden'
|
||||
FILES: Bestanden
|
||||
FROMCOMPUTER: 'Selecteer bestand op computer'
|
||||
FROMCOMPUTERINFO: 'Upload from your computer'
|
||||
@ -175,16 +175,16 @@ nl:
|
||||
XlsType: 'Excel document'
|
||||
ZipType: 'ZIP bestand'
|
||||
FileIFrameField:
|
||||
ATTACH: 'Attach {type}'
|
||||
ATTACHONCESAVED: '{type}s can be attached once you have saved the record for the first time.'
|
||||
ATTACHONCESAVED2: 'Files can be attached once you have saved the record for the first time.'
|
||||
DELETE: 'Delete {type}'
|
||||
ATTACH: 'Toevoegen {type}'
|
||||
ATTACHONCESAVED: '{type}en kunnen worden toegevoegd na het voor het eerst opslaan.'
|
||||
ATTACHONCESAVED2: 'Bestanden kunnen worden toegevoegd na het voor het eerst opslaan.'
|
||||
DELETE: 'Verwijderen {type}'
|
||||
DISALLOWEDFILETYPE: 'Dit type bestand mag niet worden opgeslagen'
|
||||
FILE: Bestand
|
||||
FROMCOMPUTER: 'Vanaf computer'
|
||||
FROMFILESTORE: 'Vanaf de website''s bestandsopslag'
|
||||
NOSOURCE: 'Selecteer een bron bestand om toe te voegen'
|
||||
REPLACE: 'Replace {type}'
|
||||
REPLACE: 'Vervang {type}'
|
||||
FileIFrameField_iframe.ss:
|
||||
TITLE: 'Afbeelding uploaden'
|
||||
Filesystem:
|
||||
@ -232,7 +232,7 @@ nl:
|
||||
DeletePermissionsFailure: 'Onvoldoende rechten om te verwijderen'
|
||||
GridFieldDetailForm:
|
||||
CancelBtn: Annuleren
|
||||
Create: Create
|
||||
Create: Creëren
|
||||
Delete: Verwijder
|
||||
DeletePermissionsFailure: 'No delete permissions'
|
||||
Deleted: '%s %s verwijderd'
|
||||
@ -334,7 +334,7 @@ nl:
|
||||
VersionUnknown: onbekend
|
||||
LeftAndMain_Menu.ss:
|
||||
Hello: Hi
|
||||
LOGOUT: 'Log out'
|
||||
LOGOUT: Uitloggen
|
||||
LoginAttempt:
|
||||
Email: 'Email adres '
|
||||
IP: 'IP Adres'
|
||||
@ -410,7 +410,7 @@ nl:
|
||||
MemberImportForm:
|
||||
Help1: '<p>Importeer leden in <em>CSV</em> formaat (Kommagescheiden bestandsformaat). <small><a href="#" class="toggle-advanced">Toon geavanceerd gebruik</a></small></p>'
|
||||
Help2: '<div class="advanced"> <h4>Advanced usage</h4> <ul> <li>Allowed columns: <em>%s</em></li> <li>Existing users are matched by their unique <em>Code</em> property, and updated with any new values from the imported file.</li> <li>Groups can be assigned by the <em>Groups</em> column. Groups are identified by their <em>Code</em> property, multiple groups can be separated by comma. Existing group memberships are not cleared.</li> </ul></div>'
|
||||
ResultCreated: 'Created {count} members'
|
||||
ResultCreated: '{count} leden aangemaakt'
|
||||
ResultDeleted: '%d leden verwijderd'
|
||||
ResultNone: 'Geen wijzingen'
|
||||
ResultUpdated: 'Updated {count} members'
|
||||
@ -551,13 +551,13 @@ nl:
|
||||
ATTACHFILE: 'Voeg een bestand toe'
|
||||
ATTACHFILES: 'Voeg bestanden toe'
|
||||
AttachFile: 'Voeg bestanden toe'
|
||||
DELETE: 'Delete from files'
|
||||
DELETE: 'Volledig verwijderen'
|
||||
DELETEINFO: 'Verwijder dit bestand uit bestandsopslag van de website.'
|
||||
DOEDIT: Bewaar
|
||||
DROPFILE: 'Bestand hiernaar toe slepen'
|
||||
DROPFILES: 'Sleep hier je bestanden'
|
||||
Dimensions: Dimensions
|
||||
EDIT: Edit
|
||||
EDIT: Bewerken
|
||||
EDITINFO: 'Bewerk dit bestand'
|
||||
FIELDNOTSET: 'Bestandsinformatie niet gevonden'
|
||||
FROMCOMPUTER: 'Vanaf computer'
|
||||
|
@ -77,7 +77,7 @@ uk:
|
||||
CHANGEPASSWORDTEXT2: 'Тепер Ви можете використовувати наступні дані для входу:'
|
||||
EMAIL: Email
|
||||
HELLO: Привіт
|
||||
PASSWORD: Password
|
||||
PASSWORD: Пароль
|
||||
CheckboxField:
|
||||
- 'False'
|
||||
- 'True'
|
||||
@ -240,7 +240,7 @@ uk:
|
||||
Saved: 'Saved %s %s'
|
||||
GridFieldItemEditView.ss: null
|
||||
Group:
|
||||
AddRole: 'Add a role for this group'
|
||||
AddRole: 'Додати роль до цієї групи'
|
||||
Code: 'Код групи'
|
||||
DefaultGroupTitleAdministrators: Адміністратори
|
||||
DefaultGroupTitleContentAuthors: 'Content Authors'
|
||||
@ -250,7 +250,7 @@ uk:
|
||||
NoRoles: 'Ролі не знайдені'
|
||||
PLURALNAME: Groups
|
||||
Parent: 'Батьківська група'
|
||||
RolesAddEditLink: 'Manage roles'
|
||||
RolesAddEditLink: 'Керувати ролями'
|
||||
SINGULARNAME: Group
|
||||
Sort: 'Порядок сортування'
|
||||
has_many_Permissions: Права
|
||||
@ -501,7 +501,7 @@ uk:
|
||||
EDITPERMISSIONS_HELP: 'Ability to edit Permissions and IP Addresses for a group. Requires the "Access to ''Security'' section" permission.'
|
||||
GROUPNAME: 'Назва групи'
|
||||
IMPORTGROUPS: 'Імпортувати групи'
|
||||
IMPORTUSERS: 'Import users'
|
||||
IMPORTUSERS: 'Імпортувати користувачів'
|
||||
MEMBERS: Члени
|
||||
MENUTITLE: Security
|
||||
MemberListCaution: 'Caution: Removing members from this list will remove them from all groups and the database'
|
||||
|
@ -76,8 +76,7 @@ class Aggregate extends ViewableData {
|
||||
* @return SQLQuery
|
||||
*/
|
||||
protected function query($attr) {
|
||||
$singleton = singleton($this->type);
|
||||
$query = $singleton->buildSQL($this->filter);
|
||||
$query = DataList::create($this->type)->where($this->filter);
|
||||
$query->setSelect($attr);
|
||||
$query->setOrderBy(array());
|
||||
$singleton->extend('augmentSQL', $query);
|
||||
|
@ -66,6 +66,9 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta
|
||||
* @return ArrayIterator
|
||||
*/
|
||||
public function getIterator() {
|
||||
foreach($this->items as $i => $item) {
|
||||
if(is_array($item)) $this->items[$i] = new ArrayData($item);
|
||||
}
|
||||
return new ArrayIterator($this->items);
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@ class DB {
|
||||
|
||||
$key = md5($key); // Ensure key is correct length for chosen cypher
|
||||
$ivSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CFB);
|
||||
$iv = mcrypt_create_iv($ivSize);
|
||||
$iv = mcrypt_create_iv($ivSize);
|
||||
$encrypted = mcrypt_encrypt(
|
||||
MCRYPT_RIJNDAEL_256, $key, $name, MCRYPT_MODE_CFB, $iv
|
||||
);
|
||||
|
@ -664,7 +664,10 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
} else {
|
||||
$item = Injector::inst()->create($defaultClass, $row, false, $this->model);
|
||||
}
|
||||
|
||||
|
||||
//set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
|
||||
$item->setSourceQueryParams($this->dataQuery()->getQueryParams());
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
|
@ -395,9 +395,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
/**
|
||||
* Set the DataModel
|
||||
* @param DataModel $model
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function setDataModel(DataModel $model) {
|
||||
$this->model = $model;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -423,12 +426,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$clone = new $className( $this->toMap(), false, $this->model );
|
||||
$clone->ID = 0;
|
||||
|
||||
$clone->extend('onBeforeDuplicate', $this, $doWrite);
|
||||
$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
|
||||
if($doWrite) {
|
||||
$clone->write();
|
||||
$this->duplicateManyManyRelations($this, $clone);
|
||||
}
|
||||
$clone->extend('onAfterDuplicate', $this, $doWrite);
|
||||
$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
@ -503,6 +506,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* or destroy and reinstanciate the record.
|
||||
*
|
||||
* @param string $className The new ClassName attribute (a subclass of {@link DataObject})
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function setClassName($className) {
|
||||
$className = trim($className);
|
||||
@ -510,6 +514,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
$this->class = $className;
|
||||
$this->setField("ClassName", $className);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -745,6 +750,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* the related objects that it alters.
|
||||
*
|
||||
* @param array $data A map of field name to data values to update.
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function update($data) {
|
||||
foreach($data as $k => $v) {
|
||||
@ -784,6 +790,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$this->$k = $v;
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -793,11 +800,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* use the write() method.
|
||||
*
|
||||
* @param array $data A map of field name to data values to update.
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function castedUpdate($data) {
|
||||
foreach($data as $k => $v) {
|
||||
$this->setCastedField($k,$v);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -895,6 +904,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Forces the record to think that all its data has changed.
|
||||
* Doesn't write to the database. Only sets fields as changed
|
||||
* if they are not already marked as changed.
|
||||
*
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function forceChange() {
|
||||
// Ensure lazy fields loaded
|
||||
@ -913,6 +924,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
// @todo Find better way to allow versioned to write a new version after forceChange
|
||||
if($this->isChanged('Version')) unset($this->changed['Version']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -987,7 +999,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Will traverse the defaults of the current class and all its parent classes.
|
||||
* Called by the constructor when creating new records.
|
||||
*
|
||||
* @uses DataExtension->populateDefaults()
|
||||
* @uses DataExtension->populateDefaults()
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function populateDefaults() {
|
||||
$classes = array_reverse(ClassInfo::ancestry($this));
|
||||
@ -1018,6 +1031,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
|
||||
$this->extend('populateDefaults');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1065,7 +1079,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
if($writeException) {
|
||||
// Used by DODs to clean up after themselves, eg, Versioned
|
||||
$this->extend('onAfterSkippedWrite');
|
||||
$this->invokeWithExtensions('onAfterSkippedWrite');
|
||||
throw $writeException;
|
||||
return false;
|
||||
}
|
||||
@ -1208,7 +1222,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
} elseif ( $showDebug ) {
|
||||
echo "<b>Debug:</b> no changes for DataObject<br />";
|
||||
// Used by DODs to clean up after themselves, eg, Versioned
|
||||
$this->extend('onAfterSkippedWrite');
|
||||
$this->invokeWithExtensions('onAfterSkippedWrite');
|
||||
}
|
||||
|
||||
// Clears the cache for this object so get_one returns the correct object.
|
||||
@ -1220,7 +1234,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$this->record['LastEdited'] = SS_Datetime::now()->Rfc2822();
|
||||
} else {
|
||||
// Used by DODs to clean up after themselves, eg, Versioned
|
||||
$this->extend('onAfterSkippedWrite');
|
||||
$this->invokeWithExtensions('onAfterSkippedWrite');
|
||||
}
|
||||
|
||||
// Write relations as necessary
|
||||
@ -1235,13 +1249,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* same record.
|
||||
*
|
||||
* @param $recursive Recursively write components
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function writeComponents($recursive = false) {
|
||||
if(!$this->components) return;
|
||||
if(!$this->components) return $this;
|
||||
|
||||
foreach($this->components as $component) {
|
||||
$component->write(false, false, false, $recursive);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1434,7 +1450,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$combinedFilter = "\"$joinField\" = '$id'";
|
||||
if(!empty($filter)) $combinedFilter .= " AND ({$filter})";
|
||||
|
||||
return singleton($componentClass)->extendedSQL($combinedFilter, $sort, $limit, $join);
|
||||
return DataList::create($componentClass)
|
||||
->where($combinedFilter)
|
||||
->canSortBy($sort)
|
||||
->limit($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2100,9 +2119,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
// Limit query to the current record, unless it has the Versioned extension,
|
||||
// in which case it requires special handling through augmentLoadLazyFields()
|
||||
if (!isset($this->record['Version'])){
|
||||
if(!$this->hasExtension('Versioned')) {
|
||||
$dataQuery->where("\"$tableClass\".\"ID\" = {$this->record['ID']}")->limit(1);
|
||||
}
|
||||
|
||||
$columns = array();
|
||||
|
||||
// Add SQL for fields, both simple & multi-value
|
||||
@ -2116,7 +2136,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
if ($columns) {
|
||||
$query = $dataQuery->query();
|
||||
$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this->record);
|
||||
$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
|
||||
$this->extend('augmentSQL', $query, $dataQuery);
|
||||
|
||||
$dataQuery->setQueriedColumns($columns);
|
||||
@ -2227,6 +2247,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*
|
||||
* @param string $fieldName Name of the field
|
||||
* @param mixed $val New field value
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function setField($fieldName, $val) {
|
||||
// Situation 1: Passing an DBField
|
||||
@ -2270,6 +2291,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$this->record[$fieldName] = $val;
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2280,6 +2302,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*
|
||||
* @param string $fieldName Name of the field
|
||||
* @param mixed $value New field value
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function setCastedField($fieldName, $val) {
|
||||
if(!$fieldName) {
|
||||
@ -2293,6 +2316,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
} else {
|
||||
$this->$fieldName = $val;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2829,14 +2853,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*
|
||||
* @param boolean $persistent When true will also clear persistent data stored in the Cache system.
|
||||
* When false will just clear session-local cached data
|
||||
*
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function flushCache($persistent = true) {
|
||||
if($persistent) Aggregate::flushCache($this->class);
|
||||
|
||||
if($this->class == 'DataObject') {
|
||||
DataObject::$_cache_get_one = array();
|
||||
return;
|
||||
return $this;
|
||||
}
|
||||
|
||||
$classes = ClassInfo::ancestry($this->class);
|
||||
@ -2847,6 +2871,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$this->extend('flushCache');
|
||||
|
||||
$this->components = array();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2907,6 +2932,46 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
return array_shift($tableClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @var Array Parameters used in the query that built this object.
|
||||
* This can be used by decorators (e.g. lazy loading) to
|
||||
* run additional queries using the same context.
|
||||
*/
|
||||
protected $sourceQueryParams;
|
||||
|
||||
/**
|
||||
* @see $sourceQueryParams
|
||||
* @return array
|
||||
*/
|
||||
public function getSourceQueryParams() {
|
||||
return $this->sourceQueryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see $sourceQueryParams
|
||||
* @param array
|
||||
*/
|
||||
public function setSourceQueryParams($array) {
|
||||
$this->sourceQueryParams = $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see $sourceQueryParams
|
||||
* @param array
|
||||
*/
|
||||
public function setSourceQueryParam($key, $value) {
|
||||
$this->sourceQueryParams[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see $sourceQueryParams
|
||||
* @return Mixed
|
||||
*/
|
||||
public function getSourceQueryParam($key) {
|
||||
if(isset($this->sourceQueryParams[$key])) return $this->sourceQueryParams[$key];
|
||||
else return null;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------------------------//
|
||||
|
||||
/**
|
||||
|
@ -111,7 +111,7 @@ class DataQuery {
|
||||
user_error("DataObjects have been requested before the manifest is loaded. Please ensure you are not"
|
||||
. " querying the database in _config.php.", E_USER_ERROR);
|
||||
} else {
|
||||
user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for"
|
||||
user_error("DataList::create Can't find data classes (classes linked to tables) for"
|
||||
. " $this->dataClass. Please ensure you run dev/build after creating a new DataObject.",
|
||||
E_USER_ERROR);
|
||||
}
|
||||
@ -743,6 +743,14 @@ class DataQuery {
|
||||
if(isset($this->queryParams[$key])) return $this->queryParams[$key];
|
||||
else return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all query parameters
|
||||
* @return array query parameters array
|
||||
*/
|
||||
public function getQueryParams() {
|
||||
return $this->queryParams;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,15 +73,13 @@ class DatabaseAdmin extends Controller {
|
||||
|
||||
|
||||
/**
|
||||
* Display a simple HTML menu of database admin helpers.
|
||||
* When we're called as /dev/build, that's actually the index. Do the same
|
||||
* as /dev/build/build.
|
||||
*/
|
||||
public function index() {
|
||||
echo "<h2>Database Administration Helpers</h2>";
|
||||
echo "<p><a href=\"build\">Add missing database fields (similar to sanity check).</a></p>";
|
||||
echo "<p><a href=\"../images/flush\">Flush <b>all</b> of the generated images.</a></p>";
|
||||
return $this->build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the database schema, creating tables & fields as necessary.
|
||||
*/
|
||||
|
@ -210,9 +210,38 @@ class ManyManyList extends RelationList {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Array Map of field => fieldtype
|
||||
* Gets the join table used for the relationship.
|
||||
*
|
||||
* @return string the name of the table
|
||||
*/
|
||||
function getExtraFields() {
|
||||
public function getJoinTable() {
|
||||
return $this->joinTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the key used to store the ID of the local/parent object.
|
||||
*
|
||||
* @return string the field name
|
||||
*/
|
||||
public function getLocalKey() {
|
||||
return $this->localKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the key used to store the ID of the foreign/child object.
|
||||
*
|
||||
* @return string the field name
|
||||
*/
|
||||
public function getForeignKey() {
|
||||
return $this->foreignKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extra fields included in the relationship.
|
||||
*
|
||||
* @return array a map of field names to types
|
||||
*/
|
||||
public function getExtraFields() {
|
||||
return $this->extraFields;
|
||||
}
|
||||
|
||||
|
@ -136,7 +136,7 @@ class Versioned extends DataExtension {
|
||||
* @todo Should this all go into VersionedDataQuery?
|
||||
*/
|
||||
public function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery = null) {
|
||||
$baseTable = ClassInfo::baseDataClass($dataQuery->dataClass());
|
||||
$baseTable = ClassInfo::baseDataClass($dataQuery->dataClass());
|
||||
|
||||
switch($dataQuery->getQueryParam('Versioned.mode')) {
|
||||
// Noop
|
||||
@ -277,14 +277,26 @@ class Versioned extends DataExtension {
|
||||
* For lazy loaded fields requiring extra sql manipulation, ie versioning
|
||||
* @param SQLQuery $query
|
||||
* @param DataQuery $dataQuery
|
||||
* @param array $record
|
||||
* @param DataObject $dataObject
|
||||
*/
|
||||
function augmentLoadLazyFields(SQLQuery &$query, DataQuery &$dataQuery = null, $record) {
|
||||
function augmentLoadLazyFields(SQLQuery &$query, DataQuery &$dataQuery = null, $dataObject) {
|
||||
// The VersionedMode local variable ensures that this decorator only applies to
|
||||
// queries that have originated from the Versioned object, and have the Versioned
|
||||
// metadata set on the query object. This prevents regular queries from
|
||||
// accidentally querying the *_versions tables.
|
||||
$versionedMode = $dataObject->getSourceQueryParam('Versioned.mode');
|
||||
$dataClass = $dataQuery->dataClass();
|
||||
if (isset($record['Version'])){
|
||||
$dataQuery->where("\"$dataClass\".\"RecordID\" = " . $record['ID']);
|
||||
$dataQuery->where("\"$dataClass\".\"Version\" = " . $record['Version']);
|
||||
$modesToAllowVersioning = array('all_versions', 'latest_versions', 'archive');
|
||||
if(
|
||||
!empty($dataObject->Version) &&
|
||||
(!empty($versionedMode) && in_array($versionedMode,$modesToAllowVersioning))
|
||||
) {
|
||||
$dataQuery->where("\"$dataClass\".\"RecordID\" = " . $dataObject->ID);
|
||||
$dataQuery->where("\"$dataClass\".\"Version\" = " . $dataObject->Version);
|
||||
$dataQuery->setQueryParam('Versioned.mode', 'all_versions');
|
||||
} else {
|
||||
// Same behaviour as in DataObject->loadLazyFields
|
||||
$dataQuery->where("\"$dataClass\".\"ID\" = {$dataObject->ID}")->limit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -312,7 +312,7 @@ class Date extends DBField {
|
||||
* @return boolean
|
||||
*/
|
||||
public function InPast() {
|
||||
return strtotime($this->value) < time();
|
||||
return strtotime($this->value) < SS_Datetime::now()->Format('U');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -320,7 +320,7 @@ class Date extends DBField {
|
||||
* @return boolean
|
||||
*/
|
||||
public function InFuture() {
|
||||
return strtotime($this->value) > time();
|
||||
return strtotime($this->value) > SS_Datetime::now()->Format('U');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -328,7 +328,7 @@ class Date extends DBField {
|
||||
* @return boolean
|
||||
*/
|
||||
public function IsToday() {
|
||||
return (date('Y-m-d', strtotime($this->value)) == date('Y-m-d', time()));
|
||||
return (date('Y-m-d', strtotime($this->value)) == SS_Datetime::now()->Format('Y-m-d'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -136,8 +136,21 @@ class HTMLText extends Text {
|
||||
return ShortcodeParser::get_active()->parse($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the field has meaningful content.
|
||||
* Excludes null content like <h1></h1>, <p></p> ,etc
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function exists() {
|
||||
return parent::exists() && $this->value != '<p></p>';
|
||||
// If it's blank, it's blank
|
||||
if(!parent::exists()) return false;
|
||||
// If it's got a content tag
|
||||
if(preg_match('/<(img|embed|object|iframe)[^>]*>/i', $this->value)) return true;
|
||||
// If it's just one or two tags on its own (and not the above) it's empty. This might be <p></p> or <h1></h1> or whatever.
|
||||
if(preg_match('/^[\\s]*(<[^>]+>[\\s]*){1,2}$/', $this->value)) return false;
|
||||
// Otherwise its content is genuine content
|
||||
return true;
|
||||
}
|
||||
|
||||
public function scaffoldFormField($title = null, $params = null) {
|
||||
|
@ -32,6 +32,19 @@ class Varchar extends StringField {
|
||||
parent::__construct($name, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow the ability to access the size of the field programatically. This
|
||||
* can be useful if you want to have text fields with a length limit that
|
||||
* is dictated by the DB field.
|
||||
*
|
||||
* TextField::create('Title')->setMaxLength(singleton('SiteTree')->dbObject('Title')->getSize())
|
||||
*
|
||||
* @return int The size of the field
|
||||
*/
|
||||
public function getSize() {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* (non-PHPdoc)
|
||||
* @see DBField::requireField()
|
||||
|
@ -2,65 +2,17 @@
|
||||
/**
|
||||
* A simple parser that allows you to map BBCode-like "shortcodes" to an arbitrary callback.
|
||||
*
|
||||
* * The Shortcode API (new in 2.4) is a simple regex based parser that allows you to replace simple bbcode-like tags
|
||||
* within a HTMLText or HTMLVarchar field when rendered into a template. It is inspired by and very similar to the
|
||||
* [Wordpress implementation](http://codex.wordpress.org/Shortcode_API) of shortcodes. Examples of shortcode tags are:
|
||||
*
|
||||
* <code>
|
||||
* [shortcode]
|
||||
* [shortcode /]
|
||||
* [shortcode parameter="value"]
|
||||
* [shortcode parameter="value"]Enclosed Content[/shortcode]
|
||||
* </code>
|
||||
*
|
||||
* <b>Defining Custom Shortcodes</b>
|
||||
*
|
||||
* All you need to do to define a shortcode is to register a callback with the parser that will be called whenever a
|
||||
* shortcode is encountered. This callback will return a string to replace the shortcode with.
|
||||
*
|
||||
* To register a shortcode you call:
|
||||
*
|
||||
* <code>
|
||||
* ShortcodeParser::get('default')->register('shortcode_tag_name', 'callback');
|
||||
* </code>
|
||||
*
|
||||
* These parameters are passed to the callback:
|
||||
* - Any parameters attached to the shortcode as an associative array (keys are lower-case).
|
||||
* - Any content enclosed within the shortcode (if it is an enclosing shortcode). Note that any content within this
|
||||
* will not have been parsed, and can optionally be fed back into the parser.
|
||||
* - The ShortcodeParser instance used to parse the content.
|
||||
* - The shortcode tag name that was matched within the parsed content.
|
||||
*
|
||||
* <b>Inbuilt Shortcodes</b>
|
||||
*
|
||||
* From 2.4 onwards links inserted via the CMS into a content field are in the form ''<a href="[sitetree_link id=n]">
|
||||
* '', and from 3.0 the comma is used as a separator instead ''<a href="[sitetree_link,id=n]">''. At runtime this is
|
||||
* replaced by a plain link to the page with the ID in question.
|
||||
*
|
||||
* <b>Limitations</b>
|
||||
*
|
||||
* Since the shortcode parser is based on a simple regular expression it cannot properly handle nested shortcodes. For
|
||||
* example the below code will not work as expected:
|
||||
*
|
||||
* <code>
|
||||
* [shortcode]
|
||||
* [shortcode][/shortcode]
|
||||
* [/shortcode]
|
||||
* </code>
|
||||
*
|
||||
* The parser will recognise this as:
|
||||
*
|
||||
* <code>
|
||||
* [shortcode]
|
||||
* [shortcode]
|
||||
* [/shortcode]
|
||||
* </code>
|
||||
* See the documentation at docs/reference/shortcodes.md .
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage misc
|
||||
*/
|
||||
class ShortcodeParser {
|
||||
|
||||
public function img_shortcode($attrs) {
|
||||
return "<img src='".$attrs['src']."'>";
|
||||
}
|
||||
|
||||
private static $instances = array();
|
||||
|
||||
private static $active_instance = 'default';
|
||||
@ -148,8 +100,362 @@ class ShortcodeParser {
|
||||
$this->shortcodes = array();
|
||||
}
|
||||
|
||||
public function callShortcode($tag, $attributes, $content) {
|
||||
if (!isset($this->shortcodes[$tag])) return false;
|
||||
return call_user_func($this->shortcodes[$tag], $attributes, $content, $this, $tag);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------
|
||||
|
||||
protected function removeNode($node) {
|
||||
$node->parentNode->removeChild($node);
|
||||
}
|
||||
|
||||
protected function insertAfter($new, $after) {
|
||||
$parent = $after->parentNode; $next = $after->nextSibling;
|
||||
|
||||
if ($next) {
|
||||
$parent->insertBefore($new, $next);
|
||||
}
|
||||
else {
|
||||
$parent->appendChild($new);
|
||||
}
|
||||
}
|
||||
|
||||
protected function insertListAfter($new, $after) {
|
||||
$doc = $after->ownerDocument; $parent = $after->parentNode; $next = $after->nextSibling;
|
||||
|
||||
for ($i = 0; $i < $new->length; $i++) {
|
||||
$imported = $doc->importNode($new->item($i), true);
|
||||
|
||||
if ($next) {
|
||||
$parent->insertBefore($imported, $next);
|
||||
}
|
||||
else {
|
||||
$parent->appendChild($imported);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static $marker_class = '--ss-shortcode-marker';
|
||||
|
||||
private static $block_level_elements = array(
|
||||
'address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'div', 'dl', 'fieldset', 'figcaption',
|
||||
'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'ol', 'output', 'p',
|
||||
'pre', 'section', 'table', 'ul'
|
||||
);
|
||||
|
||||
private static $tagrx = '/
|
||||
<(?<element>(?:"[^"]*"[\'"]*|\'[^\']*\'[\'"]*|[^\'">])+)> | # HTML Tag - skip attribute scoped tags
|
||||
\[ (?<escaped>\[.*?\]) \] | # Escaped block
|
||||
\[ (?<open>\w+) (?<attrs>.*?) (?<selfclosed>\/?) \] | # Opening tag
|
||||
\[\/ (?<close>\w+) \] # Closing tag
|
||||
/x';
|
||||
|
||||
private static $attrrx = '/
|
||||
([^\s\/\'"=,]+) # Name
|
||||
\s* = \s*
|
||||
(?:
|
||||
(?:\'([^\']+)\') | # Value surrounded by \'
|
||||
(?:"([^"]+)") | # Value surrounded by "
|
||||
(\w+) # Bare value
|
||||
)
|
||||
/x';
|
||||
|
||||
|
||||
const WARN = 'warn';
|
||||
const STRIP = 'strip';
|
||||
const LEAVE = 'leave';
|
||||
const ERROR = 'error';
|
||||
|
||||
public static $error_behavior = self::LEAVE;
|
||||
|
||||
|
||||
/**
|
||||
* Look through a string that contains shortcode tags and pull out the locations and details
|
||||
* of those tags
|
||||
*
|
||||
* Doesn't support nested shortcode tags
|
||||
*
|
||||
* @param string $content
|
||||
* @return array - The list of tags found. When using an open/close pair, only one item will be in the array,
|
||||
* with "content" set to the text between the tags
|
||||
*/
|
||||
protected function extractTags(&$content) {
|
||||
$tags = array();
|
||||
$escapes = array();
|
||||
|
||||
if(preg_match_all(self::$tagrx, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
|
||||
foreach($matches as $match) {
|
||||
// Record escaped tags
|
||||
if (!empty($match['escaped'][0])) {
|
||||
$escapes[] = array(
|
||||
's' => $match[0][1],
|
||||
'e' => $match[0][1] + strlen($match[0][0]),
|
||||
'content' => $match['escaped']
|
||||
);
|
||||
}
|
||||
|
||||
// Ignore any elements
|
||||
if (empty($match['open'][0]) && empty($match['close'][0])) continue;
|
||||
|
||||
// Pull the attributes out into a key/value hash
|
||||
$attrs = array();
|
||||
|
||||
if (!empty($match['attrs'][0])) {
|
||||
preg_match_all(self::$attrrx, $match['attrs'][0], $attrmatches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($attrmatches as $attr) {
|
||||
list($whole, $name, $value) = array_values(array_filter($attr));
|
||||
$attrs[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// And store the indexes, tag details, etc
|
||||
$tags[] = array(
|
||||
'text' => $match[0][0],
|
||||
's' => $match[0][1],
|
||||
'e' => $match[0][1] + strlen($match[0][0]),
|
||||
'open' => @$match['open'][0],
|
||||
'close' => @$match['close'][0],
|
||||
'attrs' => $attrs,
|
||||
'content' => ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$i = count($tags);
|
||||
while($i--) {
|
||||
if($tags[$i]['close']) {
|
||||
// If the tag just before this one isn't the related opening tag, throw an error
|
||||
$err = null;
|
||||
|
||||
if ($i == 0) {
|
||||
$err = 'Close tag "'.$tags[$i]['close'].'" is the first found tag, so has no related open tag';
|
||||
}
|
||||
else if (!$tags[$i-1]['open']) {
|
||||
$err = 'Close tag "'.$tags[$i]['close'].'" preceded by another close tag "'.$tags[$i-1]['close'].'"';
|
||||
}
|
||||
else if ($tags[$i]['close'] != $tags[$i-1]['open']) {
|
||||
$err = 'Close tag "'.$tags[$i]['close'].'" doesn\'t match preceding open tag "'.$tags[$i-1]['open'].'"';
|
||||
}
|
||||
|
||||
if($err) {
|
||||
if(self::$error_behavior == self::ERROR) user_error($err, E_USER_ERRROR);
|
||||
}
|
||||
else {
|
||||
// Otherwise, grab content between tags, save in opening tag & delete the closing one
|
||||
$tags[$i-1]['text'] = substr($content, $tags[$i-1]['s'], $tags[$i]['e'] - $tags[$i-1]['s']);
|
||||
$tags[$i-1]['content'] = substr($content, $tags[$i-1]['e'], $tags[$i]['s'] - $tags[$i-1]['e']);
|
||||
$tags[$i-1]['e'] = $tags[$i]['e'];
|
||||
unset($tags[$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$i = count($escapes);
|
||||
while($i--) {
|
||||
$escape = $escapes[$i];
|
||||
$content = substr_replace($content, $escape['content'], $escape['s'], $escape['e'] - $escape['s']);
|
||||
}
|
||||
|
||||
return array_values($tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the shortcode tags extracted by extractTags with HTML element "markers", so that
|
||||
* we can parse the resulting string as HTML and easily mutate the shortcodes in the DOM
|
||||
*
|
||||
* @param string $content - The HTML string with [tag] style shortcodes embedded
|
||||
* @param array $tags - The tags extracted by extractTags
|
||||
* @return string - The HTML string with [tag] style shortcodes replaced by markers
|
||||
*/
|
||||
protected function replaceTagsWithText($content, $tags, $generator) {
|
||||
// The string with tags replaced with markers
|
||||
$str = '';
|
||||
// The start index of the next tag, remembered as we step backwards through the list
|
||||
$li = null;
|
||||
|
||||
$i = count($tags);
|
||||
while($i--) {
|
||||
if ($li === null) $tail = substr($content, $tags[$i]['e']);
|
||||
else $tail = substr($content, $tags[$i]['e'], $li - $tags[$i]['e']);
|
||||
|
||||
$str = $generator($i, $tags[$i]). $tail . $str;
|
||||
$li = $tags[$i]['s'];
|
||||
}
|
||||
|
||||
return substr($content, 0, $tags[0]['s']) . $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the shortcodes in attribute values with the calculated content
|
||||
*
|
||||
* We don't use markers with attributes because there's no point, it's easier to do all the matching
|
||||
* in-DOM after the XML parse
|
||||
*
|
||||
* @param DOMDocument $doc
|
||||
*/
|
||||
protected function replaceAttributeTagsWithContent($doc) {
|
||||
$xp = new DOMXPath($doc);
|
||||
$attributes = $xp->query('//@*[contains(.,"[")][contains(.,"]")]');
|
||||
$parser = $this;
|
||||
|
||||
for($i = 0; $i < $attributes->length; $i++) {
|
||||
$node = $attributes->item($i);
|
||||
$tags = $this->extractTags($node->nodeValue);
|
||||
|
||||
if($tags) {
|
||||
$node->nodeValue = $this->replaceTagsWithText($node->nodeValue, $tags, function($idx, $tag) use ($parser){
|
||||
return $parser->callShortcode($tag['open'], $tag['attrs'], $tag['content']);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the element-scoped tags with markers
|
||||
*
|
||||
* @param string $content
|
||||
*/
|
||||
protected function replaceElementTagsWithMarkers($content) {
|
||||
$tags = $this->extractTags($content);
|
||||
|
||||
if($tags) {
|
||||
$markerClass = self::$marker_class;
|
||||
|
||||
$content = $this->replaceTagsWithText($content, $tags, function($idx, $tag) use ($markerClass) {
|
||||
return '<img class="'.$markerClass.'" data-tagid="'.$idx.'" />';
|
||||
});
|
||||
}
|
||||
|
||||
return array($content, $tags);
|
||||
}
|
||||
|
||||
protected function findParentsForMarkers($nodes) {
|
||||
$parents = array();
|
||||
|
||||
foreach($nodes as $node) {
|
||||
$parent = $node;
|
||||
|
||||
do {
|
||||
$parent = $parent->parentNode;
|
||||
}
|
||||
while($parent instanceof DOMElement && !in_array(strtolower($parent->tagName), self::$block_level_elements));
|
||||
|
||||
$node->setAttribute('data-parentid', count($parents));
|
||||
$parents[] = $parent;
|
||||
}
|
||||
|
||||
return $parents;
|
||||
}
|
||||
|
||||
const BEFORE = 'before';
|
||||
const AFTER = 'after';
|
||||
const SPLIT = 'split';
|
||||
const INLINE = 'inline';
|
||||
|
||||
/**
|
||||
* Given a node with represents a shortcode marker and a location string, mutates the DOM to put the
|
||||
* marker in the compliant location
|
||||
*
|
||||
* For shortcodes inserted BEFORE, that location is just before the block container that
|
||||
* the marker is in
|
||||
*
|
||||
* For shortcodes inserted AFTER, that location is just after the block container that
|
||||
* the marker is in
|
||||
*
|
||||
* For shortcodes inserted SPLIT, that location is where the marker is, but the DOM
|
||||
* is split around it up to the block container the marker is in - for instance,
|
||||
*
|
||||
* <p>A<span>B<marker />C</span>D</p>
|
||||
*
|
||||
* becomes
|
||||
*
|
||||
* <p>A<span>B</span></p><marker /><p><span>C</span>D</p>
|
||||
*
|
||||
* For shortcodes inserted INLINE, no modification is needed (but in that case the shortcode handler needs to
|
||||
* generate only inline blocks)
|
||||
*
|
||||
* @param DOMElement $node
|
||||
* @param int $location - ShortcodeParser::BEFORE, ShortcodeParser::SPLIT or ShortcodeParser::INLINE
|
||||
*/
|
||||
protected function moveMarkerToCompliantHome($node, $parent, $location) {
|
||||
// Move before block parent
|
||||
if($location == self::BEFORE) {
|
||||
$parent->parentNode->insertBefore($node, $parent);
|
||||
}
|
||||
// Move after block parent
|
||||
else if($location == self::AFTER) {
|
||||
$this->insertAfter($node, $parent);
|
||||
}
|
||||
// Split parent at node
|
||||
else if($location == self::SPLIT) {
|
||||
$at = $node; $splitee = $node->parentNode;
|
||||
|
||||
while($splitee !== $parent->parentNode) {
|
||||
$spliter = $splitee->cloneNode(false);
|
||||
|
||||
$this->insertAfter($spliter, $splitee);
|
||||
|
||||
while($at->nextSibling) {
|
||||
$spliter->appendChild($at->nextSibling);
|
||||
}
|
||||
|
||||
$at = $splitee; $splitee = $splitee->parentNode;
|
||||
}
|
||||
|
||||
$this->insertAfter($node, $parent);
|
||||
}
|
||||
// Do nothing
|
||||
else if($location == self::INLINE) {
|
||||
if(in_array(strtolower($node->tagName), self::$block_level_elements)) {
|
||||
user_error(
|
||||
'Requested to insert block tag '.$node->tagName.' inline - probably this will break HTML compliance',
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
// NOP
|
||||
}
|
||||
else {
|
||||
user_error('Unknown value for $location argument '.$location, E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a node with represents a shortcode marker and some informationabout the shortcode, call the
|
||||
* shortcode handler & replace the marker with the actual content
|
||||
*
|
||||
* @param DOMElement $node
|
||||
* @param array $tag
|
||||
*/
|
||||
protected function replaceMarkerWithContent($node, $tag) {
|
||||
$content = false;
|
||||
if($tag['open']) $content = $this->callShortcode($tag['open'], $tag['attrs'], $tag['content']);
|
||||
|
||||
if ($content === false) {
|
||||
if(self::$error_behavior == self::ERROR) {
|
||||
user_error('Unknown shortcode tag '.$tag['open'], E_USER_ERRROR);
|
||||
}
|
||||
if (self::$error_behavior == self::WARN) {
|
||||
$content = '<strong class="warning">'.$tag['text'].'</strong>';
|
||||
}
|
||||
else if (self::$error_behavior == self::LEAVE) {
|
||||
$content = $tag['text'];
|
||||
}
|
||||
else {
|
||||
// self::$error_behavior == self::STRIP - NOP
|
||||
}
|
||||
}
|
||||
|
||||
if ($content) {
|
||||
$parsed = HTML5_Parser::parseFragment($content, 'div');
|
||||
$this->insertListAfter($parsed, $node);
|
||||
}
|
||||
|
||||
$this->removeNode($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string, and replace any registered shortcodes within it with the result of the mapped callback.
|
||||
*
|
||||
@ -157,56 +463,73 @@ class ShortcodeParser {
|
||||
* @return string
|
||||
*/
|
||||
public function parse($content) {
|
||||
// If no shortcodes defined, don't try and parse any
|
||||
if(!$this->shortcodes) return $content;
|
||||
|
||||
$shortcodes = implode('|', array_map('preg_quote', array_keys($this->shortcodes)));
|
||||
$pattern = "/\[($shortcodes)(.*?)(\/\]|\](?(4)|(?:(.+?)\[\/\s*\\1\s*\]))|\])/s";
|
||||
|
||||
if(preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
|
||||
$replacements = array();
|
||||
foreach($matches as $match) {
|
||||
$prefix = $match[0][1] ? $content[$match[0][1]-1] : '';
|
||||
if(strlen($match[0][0]) + $match[0][1] < strlen($content)) {
|
||||
$suffix = $content[strlen($match[0][0]) + $match[0][1]];
|
||||
} else {
|
||||
$suffix = '';
|
||||
}
|
||||
if($prefix == '[' && $suffix == ']') {
|
||||
$replacements[] = array($match[0][0], $match[0][1]-1, strlen($match[0][0]) + 2);
|
||||
} else {
|
||||
$replacements[] = array($this->handleShortcode($match), $match[0][1], strlen($match[0][0]));
|
||||
}
|
||||
// If no content, don't try and parse it
|
||||
if (!trim($content)) return $content;
|
||||
|
||||
// First we operate in text mode, replacing any shortcodes with marker elements so that later we can
|
||||
// use a proper DOM
|
||||
list($content, $tags) = $this->replaceElementTagsWithMarkers($content);
|
||||
|
||||
// Now parse the result into a DOM
|
||||
require_once(THIRDPARTY_PATH.'/html5lib/HTML5/Parser.php');
|
||||
$bases = HTML5_Parser::parseFragment(trim($content), 'div');
|
||||
|
||||
// If we couldn't parse the HTML, error out
|
||||
if (!$bases || !$bases->length) {
|
||||
if(self::$error_behavior == self::ERROR) {
|
||||
user_error('Couldn\'t decode HTML when processing short codes', E_USER_ERRROR);
|
||||
}
|
||||
// We reverse this so that replacements don't break offsets
|
||||
foreach(array_reverse($replacements) as $replace) {
|
||||
$content = substr_replace($content, $replace[0], $replace[1], $replace[2]);
|
||||
else {
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
$res = '';
|
||||
$html = $bases->item(0)->parentNode;
|
||||
$doc = $html->ownerDocument;
|
||||
|
||||
$xp = new DOMXPath($doc);
|
||||
|
||||
// First, replace any shortcodes that are in attributes
|
||||
$this->replaceAttributeTagsWithContent($doc);
|
||||
|
||||
// Find all the element scoped shortcode markers
|
||||
$shortcodes = $xp->query('//img[@class="'.self::$marker_class.'"]');
|
||||
|
||||
// Find the parents. Do this before DOM modification, since SPLIT might cause parents to move otherwise
|
||||
$parents = $this->findParentsForMarkers($shortcodes);
|
||||
|
||||
return $content;
|
||||
foreach($shortcodes as $shortcode) {
|
||||
$tag = $tags[$shortcode->getAttribute('data-tagid')];
|
||||
$parent = $parents[$shortcode->getAttribute('data-parentid')];
|
||||
|
||||
$class = null;
|
||||
if(!empty($tag['attrs']['location'])) $class = $tag['attrs']['location'];
|
||||
else if(!empty($tag['attrs']['class'])) $class = $tag['attrs']['class'];
|
||||
|
||||
$location = self::INLINE;
|
||||
if($class == 'left' || $class == 'right') $location = self::BEFORE;
|
||||
if($class == 'center' || $class == 'leftALone') $location = self::SPLIT;
|
||||
|
||||
if(!$parent) {
|
||||
if($location !== self::INLINE) {
|
||||
user_error("Parent block for shortcode couldn't be found, but location wasn't INLINE", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->moveMarkerToCompliantHome($shortcode, $parent, $location);
|
||||
}
|
||||
|
||||
$this->replaceMarkerWithContent($shortcode, $tag);
|
||||
}
|
||||
|
||||
foreach($html->childNodes as $child) $res .= $doc->saveHTML($child);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
protected function handleShortcode($matches) {
|
||||
$shortcode = $matches[1][0];
|
||||
|
||||
$attributes = array(); // Parse attributes into into this array.
|
||||
|
||||
if(preg_match_all('/(\w+) *= *(?:([\'"])(.*?)\\2|([^ ,"\'>]+))/', $matches[2][0], $match, PREG_SET_ORDER)) {
|
||||
foreach($match as $attribute) {
|
||||
if(!empty($attribute[4])) {
|
||||
$attributes[strtolower($attribute[1])] = $attribute[4];
|
||||
} elseif(!empty($attribute[3])) {
|
||||
$attributes[strtolower($attribute[1])] = $attribute[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return call_user_func(
|
||||
$this->shortcodes[$shortcode],
|
||||
$attributes, isset($matches[4][0]) ? $matches[4][0] : '', $this, $shortcode);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,12 @@
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
#Form_EditorToolbarMediaForm {
|
||||
.ui-tabs-panel {
|
||||
padding-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fieldset {
|
||||
padding: $grid-x*2;
|
||||
overflow: auto;
|
||||
@ -135,7 +141,7 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
|
||||
.ss-uploadfield-item-info {
|
||||
position: relative;
|
||||
line-height: 30px;
|
||||
font-size: 18px;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
background-color: #5db4df;
|
||||
@include background-image(linear-gradient(top, #5db4df 0%,#5db1dd 8%,#439bcb 50%,#3f99cd 54%,#207db6 96%,#1e7cba 100%));
|
||||
@ -227,7 +233,8 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
color: #f00;
|
||||
@include ss-uploadfield-action-buttons;
|
||||
@include ss-uploadfield-action-buttons;
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
.ss-uploadfield-item-progress {
|
||||
@ -267,20 +274,27 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
|
||||
|
||||
.ss-uploadfield-item-info {
|
||||
float: left;
|
||||
margin: 34px 0 0;
|
||||
margin: 10px 0 0;
|
||||
.ss-insert-media &{
|
||||
margin: 15px 0px 0 20px;
|
||||
margin: 10px 0px 0 20px;
|
||||
}
|
||||
label{
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
line-height: 30px;
|
||||
padding: 5px 16px;
|
||||
padding: 8px 16px;
|
||||
margin-right:0px;
|
||||
}
|
||||
}
|
||||
.ss-uploadfield-fromcomputer {
|
||||
/*position: relative; */
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
.btn-icon-drive-upload-large {
|
||||
background: url(../images/drive-upload-large.png) no-repeat 0px -4px;
|
||||
width:32px;
|
||||
height:32px;
|
||||
margin-top:-12px;
|
||||
}
|
||||
}
|
||||
.ss-uploadfield-item-uploador {
|
||||
float: left;
|
||||
@ -288,7 +302,7 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
|
||||
font-size: 22px;
|
||||
padding: 0 20px;
|
||||
line-height: 70px;
|
||||
margin-top:16px;
|
||||
margin-top:4px;
|
||||
display: none;
|
||||
.ss-insert-media &{
|
||||
font-size: 18px;
|
||||
@ -302,9 +316,10 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
|
||||
border: 2px dashed $color-medium-separator;
|
||||
background: lighten($color-base,12%);
|
||||
display: none;
|
||||
height: 82px;
|
||||
height: 54px;
|
||||
width: 360px;
|
||||
float: left;
|
||||
text-align: center;
|
||||
|
||||
&.active{
|
||||
&.hover{
|
||||
@ -314,32 +329,31 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
|
||||
div {
|
||||
color:lighten($color-text, 10%);
|
||||
text-shadow: 0px 1px 0px #fff;
|
||||
background: url('../images/upload.png') 0 25px no-repeat;
|
||||
width:230px;
|
||||
background: url('../images/upload.png') 0 12px no-repeat;
|
||||
z-index:1;
|
||||
padding: 20px 0 0;
|
||||
padding: 20px 38px 0;
|
||||
line-height: 25px;
|
||||
font-size: 25px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
display:block;
|
||||
display: inline-block;
|
||||
margin:0 auto;
|
||||
span {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
z-index:-1;
|
||||
margin-top:-3px;
|
||||
}
|
||||
}
|
||||
.ss-insert-media &{
|
||||
margin-top:3px;
|
||||
height: 56px; //Design has these smaller than main upload area
|
||||
width: 277px;
|
||||
height: 54px;
|
||||
width: 277px; //Design has these smaller than main upload area
|
||||
overflow:hidden;
|
||||
|
||||
div{
|
||||
background-position:0 15px;
|
||||
background-position:0 13px;
|
||||
padding-top:22px;
|
||||
span{
|
||||
height:30px;
|
||||
height:38px;
|
||||
font-size:18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
@ -3,12 +3,4 @@
|
||||
padding: 0;
|
||||
clear: none;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.datetime .date .middleColumn {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.datetime .time .middleColumn {
|
||||
width: 10em;
|
||||
}
|
@ -593,6 +593,8 @@ $gf_grid_x: 16px;
|
||||
border:none;
|
||||
width:10px;
|
||||
margin:0 10px;
|
||||
display:inline;
|
||||
float:none;
|
||||
span {
|
||||
text-indent:-9999em;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
//*Mixin generates the generic button styling for the gridfield*/
|
||||
@mixin gridFieldButtons{
|
||||
border:none;
|
||||
border:none;
|
||||
display:block;
|
||||
text-indent:-9999em;
|
||||
width:30px;
|
||||
@ -32,7 +32,7 @@
|
||||
&.ss-uploadfield-item-cancel{
|
||||
@include border-radius(0);
|
||||
border-left:1px solid rgba(#fff, 0.2);
|
||||
margin-top:3px;
|
||||
margin-top:0px;
|
||||
cursor: pointer;
|
||||
opacity:0.9;
|
||||
&:hover{
|
||||
@ -42,9 +42,9 @@
|
||||
display: block;
|
||||
margin: 0;
|
||||
position:realtive;
|
||||
top:4px;
|
||||
}
|
||||
}
|
||||
top:8px;
|
||||
}
|
||||
}
|
||||
@include ss-uploadfield-editButton;
|
||||
}
|
||||
}
|
||||
@ -57,12 +57,12 @@
|
||||
@mixin ss-uploadfield-editButton{
|
||||
&.ss-uploadfield-item-edit {
|
||||
opacity:0.9;
|
||||
padding-top: 3px;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 0;
|
||||
height:100%;
|
||||
@include border-radius(0);
|
||||
&.ui-state-hover{
|
||||
background:none;
|
||||
background:none;
|
||||
opacity:1;
|
||||
span.toggle-details{
|
||||
opacity:1;
|
||||
@ -81,7 +81,7 @@
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
vertical-align: middle;
|
||||
&.opened {
|
||||
&.opened {
|
||||
margin-top:0;
|
||||
}
|
||||
}
|
||||
@ -90,5 +90,5 @@
|
||||
|
||||
.ui-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
@ -373,7 +373,7 @@ class Group extends DataObject {
|
||||
// without this check, a user would be able to add himself to an administrators group
|
||||
// with just access to the "Security" admin interface
|
||||
Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin") &&
|
||||
!DataObject::get("Permission", "GroupID = $this->ID AND Code = 'ADMIN'")
|
||||
!Permission::get()->filter(array('GroupID' => $this->ID, 'Code' => 'ADMIN'))->exists()
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
|
@ -383,9 +383,11 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
$member = DataObject::get_one("Member", "\"Member\".\"ID\" = '$SQL_uid'");
|
||||
|
||||
// check if autologin token matches
|
||||
$hash = $member->encryptWithUserSettings($token);
|
||||
if($member && (!$member->RememberLoginToken || $member->RememberLoginToken != $hash)) {
|
||||
$member = null;
|
||||
if($member) {
|
||||
$hash = $member->encryptWithUserSettings($token);
|
||||
if(!$member->RememberLoginToken || $member->RememberLoginToken !== $hash) {
|
||||
$member = null;
|
||||
}
|
||||
}
|
||||
|
||||
if($member) {
|
||||
@ -420,9 +422,12 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
$this->extend('memberLoggedOut');
|
||||
|
||||
$this->RememberLoginToken = null;
|
||||
Cookie::set('alc_enc', null);
|
||||
Cookie::set('alc_enc', null); // // Clear the Remember Me cookie
|
||||
Cookie::forceExpiry('alc_enc');
|
||||
|
||||
// Switch back to live in order to avoid infinite loops when redirecting to the login screen (if this login screen is versioned)
|
||||
Session::clear('readingMode');
|
||||
|
||||
$this->write();
|
||||
|
||||
// Audit logging hook
|
||||
|
@ -138,14 +138,10 @@ JS
|
||||
|
||||
if($backURL) Session::set('BackURL', $backURL);
|
||||
|
||||
if($badLoginURL = Session::get("BadLoginURL")) {
|
||||
$this->controller->redirect($badLoginURL);
|
||||
} else {
|
||||
// Show the right tab on failed login
|
||||
$loginLink = Director::absoluteURL($this->controller->Link('login'));
|
||||
if($backURL) $loginLink .= '?BackURL=' . urlencode($backURL);
|
||||
$this->controller->redirect($loginLink . '#' . $this->FormName() .'_tab');
|
||||
}
|
||||
// Show the right tab on failed login
|
||||
$loginLink = Director::absoluteURL($this->controller->Link('login'));
|
||||
if($backURL) $loginLink .= '?BackURL=' . urlencode($backURL);
|
||||
$this->controller->redirect($loginLink . '#' . $this->FormName() .'_tab');
|
||||
}
|
||||
}
|
||||
|
||||
|
10
security/PermissionFailureException.php
Normal file
10
security/PermissionFailureException.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Throw this exception to register that a user doesn't have permission to do the given action
|
||||
* and potentially redirect them to the log-in page. The exception message may be presented to the
|
||||
* user, so it shouldn't be in nerd-speak.
|
||||
*/
|
||||
class PermissionFailureException extends Exception {
|
||||
|
||||
}
|
@ -242,7 +242,10 @@ class Security extends Controller {
|
||||
// Audit logging hook
|
||||
$controller->extend('permissionDenied', $member);
|
||||
|
||||
$controller->redirect("Security/login?BackURL=" . urlencode($_SERVER['REQUEST_URI']));
|
||||
$controller->redirect(
|
||||
Config::inst()->get('Security', 'login_url')
|
||||
. "?BackURL=" . urlencode($_SERVER['REQUEST_URI'])
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -344,7 +347,6 @@ class Security extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$customCSS = project() . '/css/tabs.css';
|
||||
if(Director::fileExists($customCSS)) {
|
||||
Requirements::css($customCSS);
|
||||
@ -360,11 +362,12 @@ class Security extends Controller {
|
||||
$controller = Page_Controller::create($tmpPage);
|
||||
$controller->setDataModel($this->model);
|
||||
$controller->init();
|
||||
//Controller::$currentController = $controller;
|
||||
} else {
|
||||
$controller = $this;
|
||||
}
|
||||
|
||||
// if the controller calls Director::redirect(), this will break early
|
||||
if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
|
||||
|
||||
$content = '';
|
||||
$forms = $this->GetLoginForms();
|
||||
@ -458,6 +461,9 @@ class Security extends Controller {
|
||||
$controller = $this;
|
||||
}
|
||||
|
||||
// if the controller calls Director::redirect(), this will break early
|
||||
if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
|
||||
|
||||
$customisedController = $controller->customise(array(
|
||||
'Content' =>
|
||||
'<p>' .
|
||||
@ -517,6 +523,9 @@ class Security extends Controller {
|
||||
$controller = $this;
|
||||
}
|
||||
|
||||
// if the controller calls Director::redirect(), this will break early
|
||||
if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
|
||||
|
||||
$email = Convert::raw2xml(rawurldecode($request->param('ID')) . '.' . $request->getExtension());
|
||||
|
||||
$customisedController = $controller->customise(array(
|
||||
@ -580,6 +589,9 @@ class Security extends Controller {
|
||||
$controller = $this;
|
||||
}
|
||||
|
||||
// if the controller calls Director::redirect(), this will break early
|
||||
if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
|
||||
|
||||
// Extract the member from the URL.
|
||||
$member = null;
|
||||
if (isset($_REQUEST['m'])) {
|
||||
@ -817,17 +829,8 @@ class Security extends Controller {
|
||||
* @see set_password_encryption_algorithm()
|
||||
*/
|
||||
public static function encrypt_password($password, $salt = null, $algorithm = null, $member = null) {
|
||||
if(
|
||||
// if the password is empty, don't encrypt
|
||||
strlen(trim($password)) == 0
|
||||
// if no algorithm is provided and no default is set, don't encrypt
|
||||
|| (!$algorithm)
|
||||
) {
|
||||
$algorithm = 'none';
|
||||
} else {
|
||||
// Fall back to the default encryption algorithm
|
||||
if(!$algorithm) $algorithm = self::$encryptionAlgorithm;
|
||||
}
|
||||
// Fall back to the default encryption algorithm
|
||||
if(!$algorithm) $algorithm = self::$encryptionAlgorithm;
|
||||
|
||||
$e = PasswordEncryptor::create_for_algorithm($algorithm);
|
||||
|
||||
@ -927,8 +930,25 @@ class Security extends Controller {
|
||||
public static function set_ignore_disallowed_actions($flag) {
|
||||
self::$ignore_disallowed_actions = $flag;
|
||||
}
|
||||
|
||||
public static function ignore_disallowed_actions() {
|
||||
return self::$ignore_disallowed_actions;
|
||||
}
|
||||
|
||||
protected static $login_url = "Security/login";
|
||||
|
||||
/**
|
||||
* Set a custom log-in URL if you have built your own log-in page.
|
||||
*/
|
||||
public static function set_login_url($loginUrl) {
|
||||
self::$login_url = $loginUrl;
|
||||
}
|
||||
/**
|
||||
* Get the URL of the log-in page.
|
||||
* Defaults to Security/login but can be re-set with {@link set_login_url()}
|
||||
*/
|
||||
public static function login_url() {
|
||||
return self::$login_url;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -39,7 +39,7 @@
|
||||
*
|
||||
* <code>
|
||||
* # Quarter-hourly task (every hour at 25 minutes past) (remove space between first * and /15)
|
||||
* * /15 * * * * www-data /webroot/framework/cli-script.php /QuarterlyHourlyTask > /var/log/quarterhourlytask.log
|
||||
* * /15 * * * * www-data /webroot/framework/cli-script.php /QuarterHourlyTask > /var/log/quarterhourlytask.log
|
||||
*
|
||||
* # HourlyTask (every hour at 25 minutes past)
|
||||
* 25 * * * * www-data /webroot/framework/cli-script.php /HourlyTask > /var/log/hourlytask.log
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
|
||||
<div class="ss-uploadfield-item-info">
|
||||
<label class="ss-uploadfield-fromcomputer ss-ui-button ss-ui-action-constructive" title="<% _t('AssetUploadField.FROMCOMPUTERINFO', 'Upload from your computer') %>" data-icon="drive-upload">
|
||||
<label class="ss-uploadfield-fromcomputer ss-ui-button ss-ui-action-constructive" title="<% _t('AssetUploadField.FROMCOMPUTERINFO', 'Upload from your computer') %>" data-icon="drive-upload-large">
|
||||
<% _t('AssetUploadField.TOUPLOAD', 'Choose files to upload...') %>
|
||||
<input id="$id" name="$getName" class="$extraClass ss-uploadfield-fromcomputer-fileinput" data-config="$configString" type="file"<% if $multiple %> multiple="multiple"<% end_if %> title="<% _t('AssetUploadField.FROMCOMPUTER', 'Choose files from your computer') %>" />
|
||||
</label>
|
||||
|
@ -23,7 +23,7 @@
|
||||
<p>To get started with the SilverStripe framework:</p>
|
||||
<ol>
|
||||
<li>Create a <code>Controller</code> subclass (<a href="http://doc.silverstripe.org/framework/en/topics/controller">doc.silverstripe.org/framework/en/topics/controller</a>)</li>
|
||||
<li>Setup the routes.yml f to your <code>Controller</code> (<a href="http://doc.silverstripe.org/framework/en/reference/director#routing">doc.silverstripe.org/framework/en/reference/director#routing</a>).</li>
|
||||
<li>Setup the routes.yml to your <code>Controller</code> (<a href="http://doc.silverstripe.org/framework/en/reference/director#routing">doc.silverstripe.org/framework/en/reference/director#routing</a>).</li>
|
||||
<li>Create a template for your <code>Controller</code> (<a href="http://doc.silverstripe.org/framework/en/reference/templates">doc.silverstripe.org/framework/en/reference/templates</a>)</li>
|
||||
</ol>
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
</h4>
|
||||
|
||||
<div class="ss-uploadfield-item-info">
|
||||
<label class="ss-uploadfield-fromcomputer ss-ui-button ss-ui-action-constructive" title="<% _t('AssetUploadField.FROMCOMPUTERINFO', 'Upload from your computer') %>" data-icon="drive-upload">
|
||||
<label class="ss-uploadfield-fromcomputer ss-ui-button ss-ui-action-constructive" title="<% _t('AssetUploadField.FROMCOMPUTERINFO', 'Upload from your computer') %>" data-icon="drive-upload-large">
|
||||
<% _t('AssetUploadField.TOUPLOAD', 'Choose files to upload...') %>
|
||||
<input id="$id" name="$getName" class="$extraClass ss-uploadfield-fromcomputer-fileinput" data-config="$configString" type="file"<% if $multiple %> multiple="multiple"<% end_if %> title="<% _t('AssetUploadField.FROMCOMPUTER', 'Choose files from your computer') %>" />
|
||||
</label>
|
||||
|
@ -26,7 +26,7 @@
|
||||
<div class="ss-uploadfield-item-cancel cancel">
|
||||
<button data-icon="deleteLight" class="ss-uploadfield-item-cancel ss-uploadfield-item-remove" title="<% _t('UploadField.REMOVE', 'Remove') %>">
|
||||
<% _t('UploadField.REMOVE', 'Remove') %>
|
||||
</button></div>
|
||||
</button>
|
||||
|
||||
<div class="ss-uploadfield-item-edit edit">
|
||||
<button class="ss-uploadfield-item-edit ss-ui-button ui-corner-all" title="<% _t('UploadField.EDITINFO', 'Edit this file') %>" data-icon="pencil">
|
||||
|
@ -1,4 +1,9 @@
|
||||
<form $FormAttributes>
|
||||
<% if Message %>
|
||||
<p id="{$FormName}_error" class="message $MessageType">$Message</p>
|
||||
<% else %>
|
||||
<p id="{$FormName}_error" class="message $MessageType" style="display: none"></p>
|
||||
<% end_if %>
|
||||
<fieldset>
|
||||
<% loop Fields %>
|
||||
$FieldHolder
|
||||
|
@ -1,5 +1,5 @@
|
||||
<select $AttributesHTML>
|
||||
<% loop Options %>
|
||||
<option value="$Value"<% if Selected %> selected="selected"<% end_if %><% if Disabled %> disabled="disabled"<% end_if %>>$Title</option>
|
||||
<option value="$Value.XML"<% if Selected %> selected="selected"<% end_if %><% if Disabled %> disabled="disabled"<% end_if %>>$Title.XML</option>
|
||||
<% end_loop %>
|
||||
</select>
|
||||
|
@ -145,16 +145,55 @@ class RestfulServiceTest extends SapphireTest {
|
||||
|
||||
/**
|
||||
* Simulate cached response file for testing error requests that are supposed to have cache files
|
||||
*
|
||||
* @todo Generate the cachepath without hardcoding the cache data
|
||||
*/
|
||||
private function createFakeCachedResponse($connection, $subUrl) {
|
||||
$fullUrl = $connection->getAbsoluteRequestURL($subUrl);
|
||||
$cachedir = TEMP_FOLDER; // Default silverstripe cache
|
||||
$cache_file = md5($fullUrl); // Encoded name of cache file
|
||||
$cache_path = $cachedir."/xmlresponse_$cache_file";
|
||||
//these are the defaul values that one would expect in the
|
||||
$basicAuthStringMethod = new ReflectionMethod('RestfulServiceTest_MockErrorService', 'getBasicAuthString');
|
||||
$basicAuthStringMethod->setAccessible(true);
|
||||
$cachePathMethod = new ReflectionMethod('RestfulServiceTest_MockErrorService', 'getCachePath');
|
||||
$cachePathMethod->setAccessible(true);
|
||||
$cache_path = $cachePathMethod->invokeArgs($connection, array(array(
|
||||
$fullUrl,
|
||||
'GET',
|
||||
null,
|
||||
array(),
|
||||
array(),
|
||||
$basicAuthStringMethod->invoke($connection)
|
||||
)));
|
||||
|
||||
$cacheResponse = new RestfulService_Response("Cache response body");
|
||||
$store = serialize($cacheResponse);
|
||||
file_put_contents($cache_path, $store);
|
||||
}
|
||||
|
||||
public function testHttpHeaderParseing() {
|
||||
$headers = "content-type: text/html; charset=UTF-8\r\n".
|
||||
"Server: Funky/1.0\r\n".
|
||||
"Set-Cookie: foo=bar\r\n".
|
||||
"Set-Cookie: baz=quux\r\n".
|
||||
"Set-Cookie: bar=foo\r\n";
|
||||
$expected = array(
|
||||
'Content-Type' => 'text/html; charset=UTF-8',
|
||||
'Server' => 'Funky/1.0',
|
||||
'Set-Cookie' => array(
|
||||
'foo=bar',
|
||||
'baz=quux',
|
||||
'bar=foo'
|
||||
)
|
||||
);
|
||||
$headerFunction = new ReflectionMethod('RestfulService', 'parseRawHeaders');
|
||||
$headerFunction->setAccessible(true);
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
$headerFunction->invoke(
|
||||
new RestfulService(Director::absoluteBaseURL(),0), $headers
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RestfulServiceTest_Controller extends Controller implements TestOnly {
|
||||
|
@ -32,13 +32,12 @@ class XMLDataFormatterTest extends SapphireTest {
|
||||
$this->assertEquals('Test Company', (string) $xml->Company);
|
||||
$this->assertEquals($obj->ID, (int) $xml->ID);
|
||||
$this->assertEquals(
|
||||
'<Content><![CDATA[<a href="http://mysite.com">mysite.com</a> is a link in this HTML content.'
|
||||
. ' <![CDATA[this is some nested CDATA]]]]><![CDATA[>]]></Content>',
|
||||
'<Content><![CDATA[<a href="http://mysite.com">mysite.com</a> is a link in this HTML content.]]>'
|
||||
. '</Content>',
|
||||
$xml->Content->asXML()
|
||||
);
|
||||
$this->assertEquals(
|
||||
'<a href="http://mysite.com">mysite.com</a> is a link in this HTML content.'
|
||||
. ' <![CDATA[this is some nested CDATA]]>',
|
||||
'<a href="http://mysite.com">mysite.com</a> is a link in this HTML content.',
|
||||
(string)$xml->Content
|
||||
);
|
||||
}
|
||||
|
@ -2,4 +2,4 @@ XMLDataFormatterTest_DataObject:
|
||||
test-do:
|
||||
Name: Test DataObject
|
||||
Company: Test Company
|
||||
Content: <a href="http://mysite.com">mysite.com</a> is a link in this HTML content. <![CDATA[this is some nested CDATA]]>
|
||||
Content: <a href="http://mysite.com">mysite.com</a> is a link in this HTML content.
|
@ -5,6 +5,12 @@ class ControllerTest extends FunctionalTest {
|
||||
static $fixture_file = 'ControllerTest.yml';
|
||||
|
||||
protected $autoFollowRedirection = false;
|
||||
|
||||
protected $requiredExtensions = array(
|
||||
'ControllerTest_AccessBaseController' => array(
|
||||
'ControllerTest_AccessBaseControllerExtension'
|
||||
)
|
||||
);
|
||||
|
||||
public function testDefaultAction() {
|
||||
/* For a controller with a template, the default action will simple run that template. */
|
||||
@ -31,57 +37,143 @@ class ControllerTest extends FunctionalTest {
|
||||
}
|
||||
|
||||
public function testUndefinedActions() {
|
||||
$response = Director::test('ControllerTest_UnsecuredController/undefinedaction');
|
||||
$response = Director::test('ControllerTest_AccessUnsecuredSubController/undefinedaction');
|
||||
$this->assertEquals(404, $response->getStatusCode(), 'Undefined actions return a not found response.');
|
||||
}
|
||||
|
||||
public function testAllowedActions() {
|
||||
$adminUser = $this->objFromFixture('Member', 'admin');
|
||||
|
||||
$response = $this->get("ControllerTest_SecuredController/methodaction");
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$response = $this->get("ControllerTest_SecuredController/stringaction");
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
|
||||
$response = $this->get("ControllerTest_SecuredController/adminonly");
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
|
||||
$response = $this->get('ControllerTest_UnsecuredController/stringaction');
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
"test that a controller without a specified allowed_actions allows actions through"
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_FullSecuredController/index");
|
||||
$this->assertEquals(403, $response->getStatusCode(),
|
||||
"Actions can be globally disallowed by using asterisk (*) for index method"
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_FullSecuredController/adminonly");
|
||||
$this->assertEquals(404, $response->getStatusCode(),
|
||||
"Actions can be globally disallowed by using asterisk (*) instead of a method name"
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_FullSecuredController/unsecuredaction");
|
||||
$response = $this->get("ControllerTest_UnsecuredController/");
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
"Actions can be overridden to be allowed if globally disallowed by using asterisk (*)"
|
||||
'Access granted on index action without $allowed_actions on defining controller, ' .
|
||||
'when called without an action in the URL'
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_UnsecuredController/index");
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
'Access granted on index action without $allowed_actions on defining controller, ' .
|
||||
'when called with an action in the URL'
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_UnsecuredController/method1");
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
'Access granted on action without $allowed_actions on defining controller, ' .
|
||||
'when called without an action in the URL'
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_AccessBaseController/");
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
'Access granted on index with empty $allowed_actions on defining controller, ' .
|
||||
'when called without an action in the URL'
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_AccessBaseController/index");
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
'Access granted on index with empty $allowed_actions on defining controller, ' .
|
||||
'when called with an action in the URL'
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_AccessBaseController/method1");
|
||||
$this->assertEquals(403, $response->getStatusCode(),
|
||||
'Access denied on action with empty $allowed_actions on defining controller'
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_AccessBaseController/method2");
|
||||
$this->assertEquals(403, $response->getStatusCode(),
|
||||
'Access denied on action with empty $allowed_actions on defining controller, ' .
|
||||
'even when action is allowed in subclasses (allowed_actions don\'t inherit)'
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_AccessSecuredController/");
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
'Access granted on index with non-empty $allowed_actions on defining controller, ' .
|
||||
'even when index isn\'t specifically mentioned in there'
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_AccessSecuredController/method1");
|
||||
$this->assertEquals(403, $response->getStatusCode(),
|
||||
'Access denied on action which is only defined in parent controller, ' .
|
||||
'even when action is allowed in currently called class (allowed_actions don\'t inherit)'
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_AccessSecuredController/method2");
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
'Access grante on action originally defined with empty $allowed_actions on parent controller, ' .
|
||||
'because it has been redefined in the subclass'
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_AccessSecuredController/adminonly");
|
||||
$this->assertEquals(403, $response->getStatusCode(),
|
||||
'Access denied on action with $allowed_actions on defining controller, ' .
|
||||
'when restricted by unmatched permission code'
|
||||
);
|
||||
|
||||
$response = $this->get("ControllerTest_AccessSecuredController/aDmiNOnlY");
|
||||
$this->assertEquals(403, $response->getStatusCode(),
|
||||
'Access denied on action with $allowed_actions on defining controller, ' .
|
||||
'regardless of capitalization'
|
||||
);
|
||||
|
||||
$response = $this->get('ControllerTest_AccessSecuredController/protectedmethod');
|
||||
$this->assertEquals(404, $response->getStatusCode(),
|
||||
"Access denied to protected method even if its listed in allowed_actions"
|
||||
);
|
||||
|
||||
$this->session()->inst_set('loggedInAs', $adminUser->ID);
|
||||
$response = $this->get("ControllerTest_SecuredController/adminonly");
|
||||
$this->assertEquals(
|
||||
200,
|
||||
$response->getStatusCode(),
|
||||
$response = $this->get("ControllerTest_AccessSecuredController/adminonly");
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
"Permission codes are respected when set in \$allowed_actions"
|
||||
);
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
|
||||
$response = $this->get("ControllerTest_FullSecuredController/adminonly");
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
"Actions can be globally disallowed by using asterisk (*) instead of a method name"
|
||||
$response = $this->get('ControllerTest_AccessBaseController/extensionmethod1');
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
"Access granted to method defined in allowed_actions on extension, " .
|
||||
"where method is also defined on extension"
|
||||
);
|
||||
|
||||
$response = $this->get('ControllerTest_AccessSecuredController/extensionmethod1');
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
"Access granted to method defined in allowed_actions on extension, " .
|
||||
"where method is also defined on extension, even when called in a subclass"
|
||||
);
|
||||
|
||||
$response = $this->get('ControllerTest_AccessBaseController/extensionmethod2');
|
||||
$this->assertEquals(404, $response->getStatusCode(),
|
||||
"Access denied to method not defined in allowed_actions on extension, " .
|
||||
"where method is also defined on extension"
|
||||
);
|
||||
|
||||
$response = $this->get('ControllerTest_IndexSecuredController/');
|
||||
$this->assertEquals(403, $response->getStatusCode(),
|
||||
"Access denied when index action is limited through allowed_actions, " .
|
||||
"and doesn't satisfy checks, and action is empty"
|
||||
);
|
||||
|
||||
$response = $this->get('ControllerTest_IndexSecuredController/index');
|
||||
$this->assertEquals(403, $response->getStatusCode(),
|
||||
"Access denied when index action is limited through allowed_actions, " .
|
||||
"and doesn't satisfy checks"
|
||||
);
|
||||
|
||||
$this->session()->inst_set('loggedInAs', $adminUser->ID);
|
||||
$response = $this->get('ControllerTest_IndexSecuredController/');
|
||||
$this->assertEquals(200, $response->getStatusCode(),
|
||||
"Access granted when index action is limited through allowed_actions, " .
|
||||
"and does satisfy checks"
|
||||
);
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @expectedException PHPUnit_Framework_Error
|
||||
* @expectedExceptionMessage Wildcards (*) are no longer valid
|
||||
*/
|
||||
public function testWildcardAllowedActions() {
|
||||
$this->get('ControllerTest_AccessWildcardSecuredController');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Controller::join_links()
|
||||
*/
|
||||
@ -133,20 +225,60 @@ class ControllerTest extends FunctionalTest {
|
||||
*/
|
||||
public function testHasAction() {
|
||||
$controller = new ControllerTest_HasAction();
|
||||
$unsecuredController = new ControllerTest_HasAction_Unsecured();
|
||||
$securedController = new ControllerTest_AccessSecuredController();
|
||||
|
||||
$this->assertFalse($controller->hasAction('1'), 'Numeric actions do not slip through.');
|
||||
//$this->assertFalse($controller->hasAction('lowercase_permission'),
|
||||
//'Lowercase permission does not slip through.');
|
||||
$this->assertFalse($controller->hasAction('undefined'), 'undefined actions do not exist');
|
||||
$this->assertTrue($controller->hasAction('allowed_action'), 'allowed actions are recognised');
|
||||
$this->assertTrue($controller->hasAction('template_action'), 'action-specific templates are recognised');
|
||||
|
||||
$unsecured = new ControllerTest_HasAction_Unsecured();
|
||||
$this->assertFalse(
|
||||
$controller->hasAction('1'),
|
||||
'Numeric actions do not slip through.'
|
||||
);
|
||||
//$this->assertFalse(
|
||||
// $controller->hasAction('lowercase_permission'),
|
||||
// 'Lowercase permission does not slip through.'
|
||||
//);
|
||||
$this->assertFalse(
|
||||
$controller->hasAction('undefined'),
|
||||
'undefined actions do not exist'
|
||||
);
|
||||
$this->assertTrue(
|
||||
$controller->hasAction('allowed_action'),
|
||||
'allowed actions are recognised'
|
||||
);
|
||||
$this->assertTrue(
|
||||
$controller->hasAction('template_action'),
|
||||
'action-specific templates are recognised'
|
||||
);
|
||||
|
||||
$this->assertTrue (
|
||||
$unsecured->hasAction('defined_action'),
|
||||
$unsecuredController->hasAction('defined_action'),
|
||||
'Without an allowed_actions, any defined methods are recognised as actions'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
$securedController->hasAction('adminonly'),
|
||||
'Method is generally visible even if its denied via allowed_actions'
|
||||
);
|
||||
|
||||
$this->assertFalse(
|
||||
$securedController->hasAction('protectedmethod'),
|
||||
'Method is not visible when protected, even if its defined in allowed_actions'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
$securedController->hasAction('extensionmethod1'),
|
||||
'Method is visible when defined on an extension and part of allowed_actions'
|
||||
);
|
||||
|
||||
$this->assertFalse(
|
||||
$securedController->hasAction('internalextensionmethod'),
|
||||
'Method is not visible when defined on an extension, but not part of allowed_actions'
|
||||
);
|
||||
|
||||
$this->assertFalse(
|
||||
$securedController->hasAction('protectedextensionmethod'),
|
||||
'Method is not visible when defined on an extension, part of allowed_actions, ' .
|
||||
'but with protected visibility'
|
||||
);
|
||||
}
|
||||
|
||||
/* Controller::BaseURL no longer exists, but was just a direct call to Director::BaseURL, so not sure what this
|
||||
@ -209,7 +341,14 @@ class ControllerTest extends FunctionalTest {
|
||||
* Simple controller for testing
|
||||
*/
|
||||
class ControllerTest_Controller extends Controller implements TestOnly {
|
||||
|
||||
public $Content = "default content";
|
||||
|
||||
public static $allowed_actions = array(
|
||||
'methodaction',
|
||||
'stringaction',
|
||||
'redirectbacktest',
|
||||
);
|
||||
|
||||
public function methodaction() {
|
||||
return array(
|
||||
@ -226,49 +365,83 @@ class ControllerTest_Controller extends Controller implements TestOnly {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller with an $allowed_actions value
|
||||
*/
|
||||
class ControllerTest_SecuredController extends Controller implements TestOnly {
|
||||
class ControllerTest_UnsecuredController extends Controller implements TestOnly {
|
||||
|
||||
// Not defined, allow access to all
|
||||
// static $allowed_actions = array();
|
||||
|
||||
// Granted for all
|
||||
public function method1() {}
|
||||
|
||||
// Granted for all
|
||||
public function method2() {}
|
||||
}
|
||||
|
||||
class ControllerTest_AccessBaseController extends Controller implements TestOnly {
|
||||
|
||||
static $allowed_actions = array();
|
||||
|
||||
// Denied for all
|
||||
public function method1() {}
|
||||
|
||||
// Denied for all
|
||||
public function method2() {}
|
||||
}
|
||||
|
||||
class ControllerTest_AccessSecuredController extends ControllerTest_AccessBaseController implements TestOnly {
|
||||
|
||||
static $allowed_actions = array(
|
||||
"methodaction",
|
||||
"method1", // denied because only defined in parent
|
||||
"method2" => true, // granted because its redefined
|
||||
"adminonly" => "ADMIN",
|
||||
"protectedmethod" => true, // denied because its protected
|
||||
);
|
||||
|
||||
public $Content = "default content";
|
||||
|
||||
public function methodaction() {
|
||||
return array(
|
||||
"Content" => "methodaction content"
|
||||
);
|
||||
}
|
||||
|
||||
public function stringaction() {
|
||||
return "stringaction was called.";
|
||||
}
|
||||
|
||||
public function adminonly() {
|
||||
return "You must be an admin!";
|
||||
}
|
||||
public function method2() {}
|
||||
|
||||
public function adminonly() {}
|
||||
|
||||
protected function protectedmethod() {}
|
||||
|
||||
}
|
||||
|
||||
class ControllerTest_FullSecuredController extends Controller implements TestOnly {
|
||||
class ControllerTest_AccessWildcardSecuredController extends ControllerTest_AccessBaseController implements TestOnly {
|
||||
|
||||
static $allowed_actions = array(
|
||||
"*" => "ADMIN",
|
||||
'unsecuredaction' => true,
|
||||
"*" => "ADMIN", // should throw exception
|
||||
);
|
||||
|
||||
public function adminonly() {
|
||||
return "You must be an admin!";
|
||||
}
|
||||
|
||||
public function unsecuredaction() {
|
||||
return "Allowed for everybody";
|
||||
}
|
||||
}
|
||||
|
||||
class ControllerTest_UnsecuredController extends ControllerTest_SecuredController implements TestOnly {}
|
||||
class ControllerTest_IndexSecuredController extends ControllerTest_AccessBaseController implements TestOnly {
|
||||
|
||||
static $allowed_actions = array(
|
||||
"index" => "ADMIN",
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
class ControllerTest_AccessBaseControllerExtension extends Extension implements TestOnly {
|
||||
|
||||
static $allowed_actions = array(
|
||||
"extensionmethod1" => true, // granted because defined on this class
|
||||
"method1" => true, // ignored because method not defined on this class
|
||||
"method2" => true, // ignored because method not defined on this class
|
||||
"protectedextensionmethod" => true, // ignored because method is protected
|
||||
);
|
||||
|
||||
// Allowed for all
|
||||
public function extensionmethod1() {}
|
||||
|
||||
// Denied for all, not defined
|
||||
public function extensionmethod2() {}
|
||||
|
||||
// Denied because its protected
|
||||
protected function protectedextensionmethod() {}
|
||||
|
||||
public function internalextensionmethod() {}
|
||||
|
||||
}
|
||||
|
||||
class ControllerTest_HasAction extends Controller {
|
||||
|
||||
|
@ -227,7 +227,7 @@ class DirectorTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testForceSSLOnSubPagesPattern() {
|
||||
$_SERVER['REQUEST_URI'] = Director::baseURL() . 'Security/login';
|
||||
$_SERVER['REQUEST_URI'] = Director::baseURL() . Config::inst()->get('Security', 'login_url');
|
||||
$output = Director::forceSSL(array('/^Security/'));
|
||||
$this->assertEquals($output, 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ class HTTPResponseTest extends SapphireTest {
|
||||
|
||||
public function testContentLengthHeader() {
|
||||
$r = new SS_HTTPResponse('123ü');
|
||||
$r->fixContentLength();
|
||||
$this->assertNotNull($r->getHeader('Content-Length'), 'Content-length header is added');
|
||||
$this->assertEquals(
|
||||
5,
|
||||
@ -24,7 +23,6 @@ class HTTPResponseTest extends SapphireTest {
|
||||
);
|
||||
|
||||
$r->setBody('1234ü');
|
||||
$r->fixContentLength();
|
||||
$this->assertEquals(
|
||||
6,
|
||||
$r->getHeader('Content-Length'),
|
||||
|
@ -120,4 +120,82 @@ class HTTPTest extends SapphireTest {
|
||||
HTTP::get_mime_type(FRAMEWORK_DIR.'/tests/control/files/file.psd'));
|
||||
$this->assertEquals('audio/x-wav', HTTP::get_mime_type(FRAMEWORK_DIR.'/tests/control/files/file.wav'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that absoluteURLs correctly transforms urls within CSS to absolute
|
||||
*/
|
||||
public function testAbsoluteURLsCSS() {
|
||||
$this->withBaseURL('http://www.silverstripe.org/', function($test){
|
||||
|
||||
// background-image
|
||||
// Note that using /./ in urls is absolutely acceptable
|
||||
$test->assertEquals(
|
||||
'<div style="background-image: url(\'http://www.silverstripe.org/./images/mybackground.gif\');">Content</div>',
|
||||
HTTP::absoluteURLs('<div style="background-image: url(\'./images/mybackground.gif\');">Content</div>')
|
||||
);
|
||||
|
||||
// background
|
||||
$test->assertEquals(
|
||||
'<div style="background: url(\'http://www.silverstripe.org/images/mybackground.gif\');">Content</div>',
|
||||
HTTP::absoluteURLs('<div style="background: url(\'images/mybackground.gif\');">Content</div>')
|
||||
);
|
||||
|
||||
// list-style-image
|
||||
$test->assertEquals(
|
||||
'<div style=\'background: url(http://www.silverstripe.org/list.png);\'>Content</div>',
|
||||
HTTP::absoluteURLs('<div style=\'background: url(list.png);\'>Content</div>')
|
||||
);
|
||||
|
||||
// list-style
|
||||
$test->assertEquals(
|
||||
'<div style=\'background: url("http://www.silverstripe.org/./assets/list.png");\'>Content</div>',
|
||||
HTTP::absoluteURLs('<div style=\'background: url("./assets/list.png");\'>Content</div>')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that absoluteURLs correctly transforms urls within html attributes to absolute
|
||||
*/
|
||||
public function testAbsoluteURLsAttributes() {
|
||||
$this->withBaseURL('http://www.silverstripe.org/', function($test){
|
||||
|
||||
// links
|
||||
$test->assertEquals(
|
||||
'<a href=\'http://www.silverstripe.org/blog/\'>SS Blog</a>',
|
||||
HTTP::absoluteURLs('<a href=\'/blog/\'>SS Blog</a>')
|
||||
);
|
||||
|
||||
// background
|
||||
// Note that using /./ in urls is absolutely acceptable
|
||||
$test->assertEquals(
|
||||
'<div background="http://www.silverstripe.org/./themes/silverstripe/images/nav-bg-repeat-2.png">SS Blog</div>',
|
||||
HTTP::absoluteURLs('<div background="./themes/silverstripe/images/nav-bg-repeat-2.png">SS Blog</div>')
|
||||
);
|
||||
|
||||
// image
|
||||
$test->assertEquals(
|
||||
'<img src=\'http://www.silverstripe.org/themes/silverstripe/images/logo-org.png\' />',
|
||||
HTTP::absoluteURLs('<img src=\'themes/silverstripe/images/logo-org.png\' />')
|
||||
);
|
||||
|
||||
// link
|
||||
$test->assertEquals(
|
||||
'<link href=http://www.silverstripe.org/base.css />',
|
||||
HTTP::absoluteURLs('<link href=base.css />')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a test while mocking the base url with the provided value
|
||||
* @param string $url The base URL to use for this test
|
||||
* @param callable $callback The test to run
|
||||
*/
|
||||
protected function withBaseURL($url, $callback) {
|
||||
$oldBase = Director::$alternateBaseURL;
|
||||
Director::setBaseURL($url);
|
||||
$callback($this);
|
||||
Director::setBaseURL($oldBase);
|
||||
}
|
||||
}
|
||||
|
@ -65,17 +65,13 @@ class RequestHandlingTest extends FunctionalTest {
|
||||
}
|
||||
|
||||
public function testBadBase() {
|
||||
/* Without a double-slash indicator in the URL, the entire URL is popped off the stack. The controller's
|
||||
* default action handlers have been designed for this to an extend: simple actions can still be called.
|
||||
* This is the set-up of URL rules written before this new request handler. */
|
||||
/* We no longer support using hacky attempting to handle URL parsing with broken rules */
|
||||
$response = Director::test("testBadBase/method/1/2");
|
||||
$this->assertEquals("This is a method on the controller: 1, 2", $response->getBody());
|
||||
$this->assertNotEquals("This is a method on the controller: 1, 2", $response->getBody());
|
||||
|
||||
$response = Director::test("testBadBase/TestForm", array("MyField" => 3), null, "POST");
|
||||
$this->assertEquals("Form posted", $response->getBody());
|
||||
$this->assertNotEquals("Form posted", $response->getBody());
|
||||
|
||||
/* It won't, however, let you chain requests to access methods on forms, or form fields. In order to do that,
|
||||
* you need to have a // marker in your URL parsing rule */
|
||||
$response = Director::test("testBadBase/TestForm/fields/MyField");
|
||||
$this->assertNotEquals("MyField requested", $response->getBody());
|
||||
}
|
||||
@ -162,7 +158,7 @@ class RequestHandlingTest extends FunctionalTest {
|
||||
|
||||
public function testMethodsOnParentClassesOfRequestHandlerDeclined() {
|
||||
$response = Director::test('testGoodBase1/getIterator');
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testFormActionsCanBypassAllowedActions() {
|
||||
@ -295,6 +291,17 @@ Config::inst()->update('Director', 'rules', array(
|
||||
* Controller for the test
|
||||
*/
|
||||
class RequestHandlingTest_Controller extends Controller implements TestOnly {
|
||||
|
||||
static $allowed_actions = array(
|
||||
'method',
|
||||
'legacymethod',
|
||||
'virtualfile',
|
||||
'TestForm',
|
||||
'throwexception',
|
||||
'throwresponseexception',
|
||||
'throwhttperror',
|
||||
);
|
||||
|
||||
static $url_handlers = array(
|
||||
// The double-slash is need here to ensure that
|
||||
'$Action//$ID/$OtherID' => "handleAction",
|
||||
|
@ -65,6 +65,49 @@ class SS_LogTest extends SapphireTest {
|
||||
);
|
||||
}
|
||||
|
||||
protected function exceptionGeneratorThrower() {
|
||||
throw new Exception("thrown from SS_LogTest::testExceptionGeneratorTop");
|
||||
}
|
||||
|
||||
protected function exceptionGenerator() {
|
||||
$this->exceptionGeneratorThrower();
|
||||
}
|
||||
|
||||
public function testEmailException() {
|
||||
$testEmailWriter = new SS_LogEmailWriter('test@test.com');
|
||||
SS_Log::add_writer($testEmailWriter, SS_Log::ERR);
|
||||
|
||||
// Trigger exception handling mechanism
|
||||
try {
|
||||
$this->exceptionGenerator();
|
||||
} catch(Exception $exception) {
|
||||
// Mimics exceptionHandler, but without the exit(1)
|
||||
SS_Log::log(
|
||||
array(
|
||||
'errno' => E_USER_ERROR,
|
||||
'errstr' => ("Uncaught " . get_class($exception) . ": " . $exception->getMessage()),
|
||||
'errfile' => $exception->getFile(),
|
||||
'errline' => $exception->getLine(),
|
||||
'errcontext' => $exception->getTrace()
|
||||
),
|
||||
SS_Log::ERR
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure email is sent
|
||||
$this->assertEmailSent('test@test.com');
|
||||
|
||||
// Begin parsing of email body
|
||||
$email = $this->findEmail('test@test.com');
|
||||
$parser = new CSSContentParser($email['htmlContent']);
|
||||
|
||||
// Check that the first three lines of the stacktrace are correct
|
||||
$stacktrace = $parser->getByXpath('//body/div[1]/ul[1]');
|
||||
$this->assertContains('<b>SS_LogTest->exceptionGeneratorThrower()</b>', $stacktrace[0]->li[0]->asXML());
|
||||
$this->assertContains('<b>SS_LogTest->exceptionGenerator()</b>', $stacktrace[0]->li[1]->asXML());
|
||||
$this->assertContains('<b>SS_LogTest->testEmailException()</b>', $stacktrace[0]->li[2]->asXML());
|
||||
}
|
||||
|
||||
public function testSubclassedLogger() {
|
||||
$this->assertTrue(SS_Log::get_logger() !== SS_LogTest_NewLogger::get_logger());
|
||||
}
|
||||
|
@ -25,16 +25,9 @@ class DatetimeFieldTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testFormSaveInto() {
|
||||
$form = new Form(
|
||||
new Controller(),
|
||||
'Form',
|
||||
new FieldList(
|
||||
$f = new DatetimeField('MyDatetime', null)
|
||||
),
|
||||
new FieldList(
|
||||
new FormAction('doSubmit')
|
||||
)
|
||||
);
|
||||
$f = new DatetimeField('MyDatetime', null);
|
||||
$form = $this->getMockForm();
|
||||
$form->Fields()->push($f);
|
||||
$f->setValue(array(
|
||||
'date' => '29/03/2003',
|
||||
'time' => '23:59:38'
|
||||
@ -170,6 +163,65 @@ class DatetimeFieldTest extends SapphireTest {
|
||||
|
||||
date_default_timezone_set($oldTz);
|
||||
}
|
||||
|
||||
public function testSetDateField() {
|
||||
$form = $this->getMockForm();
|
||||
$field = new DatetimeField('Datetime', 'Datetime');
|
||||
$field->setForm($form);
|
||||
$field->setValue(array(
|
||||
'date' => '24/06/2003',
|
||||
'time' => '23:59:59',
|
||||
));
|
||||
$dateField = new DateField('Datetime[date]');
|
||||
$field->setDateField($dateField);
|
||||
|
||||
$this->assertEquals(
|
||||
$dateField->getForm(),
|
||||
$form,
|
||||
'Sets form on new field'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'2003-06-24',
|
||||
$dateField->dataValue(),
|
||||
'Sets existing value on new field'
|
||||
);
|
||||
}
|
||||
|
||||
public function testSetTimeField() {
|
||||
$form = $this->getMockForm();
|
||||
$field = new DatetimeField('Datetime', 'Datetime');
|
||||
$field->setForm($form);
|
||||
$field->setValue(array(
|
||||
'date' => '24/06/2003',
|
||||
'time' => '23:59:59',
|
||||
));
|
||||
$timeField = new TimeField('Datetime[time]');
|
||||
$field->setTimeField($timeField);
|
||||
|
||||
$this->assertEquals(
|
||||
$timeField->getForm(),
|
||||
$form,
|
||||
'Sets form on new field'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'23:59:59',
|
||||
$timeField->dataValue(),
|
||||
'Sets existing value on new field'
|
||||
);
|
||||
}
|
||||
|
||||
protected function getMockForm() {
|
||||
return new Form(
|
||||
new Controller(),
|
||||
'Form',
|
||||
new FieldList(),
|
||||
new FieldList(
|
||||
new FormAction('doSubmit')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -239,7 +239,7 @@ class GridFieldTest extends SapphireTest {
|
||||
public function testHandleActionBadArgument() {
|
||||
$this->setExpectedException('InvalidArgumentException');
|
||||
$obj = new GridField('testfield', 'testfield');
|
||||
$obj->handleAction('prft', array(), array());
|
||||
$obj->handleAlterAction('prft', array(), array());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -248,7 +248,7 @@ class GridFieldTest extends SapphireTest {
|
||||
public function testHandleAction() {
|
||||
$config = GridFieldConfig::create()->addComponent(new GridFieldTest_Component);
|
||||
$obj = new GridField('testfield', 'testfield', ArrayList::create(), $config);
|
||||
$this->assertEquals('handledAction is executed', $obj->handleAction('jump', array(), array()));
|
||||
$this->assertEquals('handledAction is executed', $obj->handleAlterAction('jump', array(), array()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -167,6 +167,29 @@ class RequirementsTest extends SapphireTest {
|
||||
$backend->delete_combined_files('RequirementsTest_bc.js');
|
||||
}
|
||||
|
||||
public function testCombinedCss() {
|
||||
$basePath = $this->getCurrentRelativePath();
|
||||
$backend = new Requirements_Backend;
|
||||
$backend->set_combined_files_enabled(true);
|
||||
|
||||
$backend->combine_files(
|
||||
'print.css',
|
||||
array(
|
||||
$basePath . '/RequirementsTest_print_a.css',
|
||||
$basePath . '/RequirementsTest_print_b.css'
|
||||
),
|
||||
'print'
|
||||
);
|
||||
|
||||
$html = $backend->includeInHTML(false, self::$html_template);
|
||||
|
||||
$this->assertTrue((bool)preg_match('/href=".*\/print\.css/', $html), 'Print stylesheets have been combined.');
|
||||
$this->assertTrue((bool)preg_match(
|
||||
'/media="print/', $html),
|
||||
'Combined print stylesheet retains the media parameter'
|
||||
);
|
||||
}
|
||||
|
||||
public function testBlockedCombinedJavascript() {
|
||||
$basePath = $this->getCurrentRelativePath();
|
||||
|
||||
|
0
tests/forms/RequirementsTest_print_a.css
Normal file
0
tests/forms/RequirementsTest_print_a.css
Normal file
0
tests/forms/RequirementsTest_print_b.css
Normal file
0
tests/forms/RequirementsTest_print_b.css
Normal file
@ -523,6 +523,24 @@ class InjectorTest extends SapphireTest {
|
||||
|
||||
$this->assertInstanceOf('OtherTestObject', $item->property->property);
|
||||
}
|
||||
|
||||
public function testNamedServices() {
|
||||
$injector = new Injector();
|
||||
$service = new stdClass();
|
||||
|
||||
$injector->registerNamedService('NamedService', $service);
|
||||
$this->assertEquals($service, $injector->get('NamedService'));
|
||||
}
|
||||
|
||||
public function testCreateConfiggedObjectWithCustomConstructorArgs() {
|
||||
// need to make sure that even if the config defines some constructor params,
|
||||
// that we take our passed in constructor args instead
|
||||
$injector = new Injector(array('locator' => 'InjectorTestConfigLocator'));
|
||||
|
||||
$item = $injector->create('ConfigConstructor', 'othervalue');
|
||||
$this->assertEquals($item->property, 'othervalue');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class InjectorTestConfigLocator extends SilverStripeServiceConfigurationLocator implements TestOnly {
|
||||
@ -531,6 +549,10 @@ class InjectorTestConfigLocator extends SilverStripeServiceConfigurationLocator
|
||||
return array('class' => 'ConstructableObject', 'constructor' => array('%$OtherTestObject'));
|
||||
}
|
||||
|
||||
if ($name == 'ConfigConstructor') {
|
||||
return array('class' => 'ConstructableObject', 'constructor' => array('value'));
|
||||
}
|
||||
|
||||
return parent::locateConfigFor($name);
|
||||
}
|
||||
}
|
||||
|
@ -268,6 +268,7 @@ class DataObjectLazyLoadingTest extends SapphireTest {
|
||||
$obj1->write();
|
||||
$version2 = $obj1->Version;
|
||||
|
||||
|
||||
$reloaded = Versioned::get_version('VersionedTest_Subclass', $obj1->ID, $version1);
|
||||
$this->assertEquals($reloaded->Name, 'test');
|
||||
$this->assertEquals($reloaded->ExtraField, 'foo');
|
||||
@ -275,6 +276,138 @@ class DataObjectLazyLoadingTest extends SapphireTest {
|
||||
$reloaded = Versioned::get_version('VersionedTest_Subclass', $obj1->ID, $version2);
|
||||
$this->assertEquals($reloaded->Name, 'test2');
|
||||
$this->assertEquals($reloaded->ExtraField, 'baz');
|
||||
|
||||
$reloaded = Versioned::get_latest_version('VersionedTest_Subclass', $obj1->ID);
|
||||
$this->assertEquals($reloaded->Version, $version2);
|
||||
$this->assertEquals($reloaded->Name, 'test2');
|
||||
$this->assertEquals($reloaded->ExtraField, 'baz');
|
||||
|
||||
$allVersions = Versioned::get_all_versions('VersionedTest_Subclass', $obj1->ID);
|
||||
$this->assertEquals(2, $allVersions->Count());
|
||||
$this->assertEquals($allVersions->First()->Version, $version1);
|
||||
$this->assertEquals($allVersions->First()->Name, 'test');
|
||||
$this->assertEquals($allVersions->First()->ExtraField, 'foo');
|
||||
$this->assertEquals($allVersions->Last()->Version, $version2);
|
||||
$this->assertEquals($allVersions->Last()->Name, 'test2');
|
||||
$this->assertEquals($allVersions->Last()->ExtraField, 'baz');
|
||||
|
||||
$obj1->delete();
|
||||
}
|
||||
|
||||
public function testLazyLoadedFieldsDoNotReferenceVersionsTable() {
|
||||
// Save another record, sanity check that we're getting the right one
|
||||
$obj2 = new VersionedTest_Subclass();
|
||||
$obj2->Name = "test2";
|
||||
$obj2->ExtraField = "foo2";
|
||||
$obj2->write();
|
||||
|
||||
$obj1 = new VersionedLazySub_DataObject();
|
||||
$obj1->PageName = "old-value";
|
||||
$obj1->ExtraField = "old-value";
|
||||
$obj1ID = $obj1->write();
|
||||
$obj1->publish('Stage', 'Live');
|
||||
|
||||
$obj1 = VersionedLazySub_DataObject::get()->byID($obj1ID);
|
||||
$this->assertEquals(
|
||||
'old-value',
|
||||
$obj1->PageName,
|
||||
"Correct value on base table when fetching base class"
|
||||
);
|
||||
$this->assertEquals(
|
||||
'old-value',
|
||||
$obj1->ExtraField,
|
||||
"Correct value on sub table when fetching base class"
|
||||
);
|
||||
|
||||
$obj1 = VersionedLazy_DataObject::get()->byID($obj1ID);
|
||||
$this->assertEquals(
|
||||
'old-value',
|
||||
$obj1->PageName,
|
||||
"Correct value on base table when fetching sub class"
|
||||
);
|
||||
$this->assertEquals(
|
||||
'old-value',
|
||||
$obj1->ExtraField,
|
||||
"Correct value on sub table when fetching sub class"
|
||||
);
|
||||
|
||||
// Force inconsistent state to test behaviour (shouldn't select from *_versions)
|
||||
DB::query(sprintf(
|
||||
"UPDATE \"VersionedLazy_DataObject_versions\" SET \"PageName\" = 'versioned-value' " .
|
||||
"WHERE \"RecordID\" = %d",
|
||||
$obj1ID
|
||||
));
|
||||
DB::query(sprintf(
|
||||
"UPDATE \"VersionedLazySub_DataObject_versions\" SET \"ExtraField\" = 'versioned-value' " .
|
||||
"WHERE \"RecordID\" = %d",
|
||||
$obj1ID
|
||||
));
|
||||
|
||||
$obj1 = VersionedLazySub_DataObject::get()->byID($obj1ID);
|
||||
$this->assertEquals(
|
||||
'old-value',
|
||||
$obj1->PageName,
|
||||
"Correct value on base table when fetching base class"
|
||||
);
|
||||
$this->assertEquals(
|
||||
'old-value',
|
||||
$obj1->ExtraField,
|
||||
"Correct value on sub table when fetching base class"
|
||||
);
|
||||
$obj1 = VersionedLazy_DataObject::get()->byID($obj1ID);
|
||||
$this->assertEquals(
|
||||
'old-value',
|
||||
$obj1->PageName,
|
||||
"Correct value on base table when fetching sub class"
|
||||
);
|
||||
$this->assertEquals(
|
||||
'old-value',
|
||||
$obj1->ExtraField,
|
||||
"Correct value on sub table when fetching sub class"
|
||||
);
|
||||
|
||||
// Update live table only to test behaviour (shouldn't select from *_versions or stage)
|
||||
DB::query(sprintf(
|
||||
'UPDATE "VersionedLazy_DataObject_Live" SET "PageName" = \'live-value\' WHERE "ID" = %d',
|
||||
$obj1ID
|
||||
));
|
||||
DB::query(sprintf(
|
||||
'UPDATE "VersionedLazySub_DataObject_Live" SET "ExtraField" = \'live-value\' WHERE "ID" = %d',
|
||||
$obj1ID
|
||||
));
|
||||
|
||||
Versioned::reading_stage('Live');
|
||||
$obj1 = VersionedLazy_DataObject::get()->byID($obj1ID);
|
||||
$this->assertEquals(
|
||||
'live-value',
|
||||
$obj1->PageName,
|
||||
"Correct value from base table when fetching base class on live stage"
|
||||
);
|
||||
$this->assertEquals(
|
||||
'live-value',
|
||||
$obj1->ExtraField,
|
||||
"Correct value from sub table when fetching base class on live stage"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/** Additional classes for versioned lazy loading testing */
|
||||
class VersionedLazy_DataObject extends DataObject {
|
||||
static $db = array(
|
||||
"PageName" => "Varchar"
|
||||
);
|
||||
static $extensions = array(
|
||||
"Versioned('Stage', 'Live')"
|
||||
);
|
||||
}
|
||||
|
||||
class VersionedLazySub_DataObject extends VersionedLazy_DataObject {
|
||||
static $db = array(
|
||||
"ExtraField" => "Varchar",
|
||||
);
|
||||
static $extensions = array(
|
||||
"Versioned('Stage', 'Live')"
|
||||
);
|
||||
}
|
@ -139,4 +139,38 @@ class HTMLTextTest extends SapphireTest {
|
||||
$data = DBField::create_field('HTMLText', '"this is a test"');
|
||||
$this->assertEquals($data->ATT(), '"this is a test"');
|
||||
}
|
||||
|
||||
function testExists() {
|
||||
$h = new HTMLText;
|
||||
$h->setValue("");
|
||||
$this->assertFalse($h->exists());
|
||||
$h->setValue("<p></p>");
|
||||
$this->assertFalse($h->exists());
|
||||
$h->setValue("<p> </p>");
|
||||
$this->assertFalse($h->exists());
|
||||
$h->setValue("<h2/>");
|
||||
$this->assertFalse($h->exists());
|
||||
$h->setValue("<h2></h2>");
|
||||
$this->assertFalse($h->exists());
|
||||
|
||||
$h->setValue("something");
|
||||
$this->assertTrue($h->exists());
|
||||
$h->setValue("<img src=\"dummy.png\">");
|
||||
$this->assertTrue($h->exists());
|
||||
$h->setValue("<img src=\"dummy.png\"><img src=\"dummy.png\">");
|
||||
$this->assertTrue($h->exists());
|
||||
$h->setValue("<p><img src=\"dummy.png\"></p>");
|
||||
$this->assertTrue($h->exists());
|
||||
|
||||
$h->setValue("<iframe src=\"http://www.google.com\"></iframe>");
|
||||
$this->assertTrue($h->exists());
|
||||
$h->setValue("<embed src=\"test.swf\">");
|
||||
$this->assertTrue($h->exists());
|
||||
$h->setValue("<object width=\"400\" height=\"400\" data=\"test.swf\"></object>");
|
||||
$this->assertTrue($h->exists());
|
||||
|
||||
|
||||
$h->setValue("<p>test</p>");
|
||||
$this->assertTrue($h->exists());
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,19 @@ class ShortcodeParserTest extends SapphireTest {
|
||||
* Tests that valid short codes that have not been registered are not replaced.
|
||||
*/
|
||||
public function testNotRegisteredShortcode() {
|
||||
ShortcodeParser::$error_behavior = ShortcodeParser::STRIP;
|
||||
$this->assertEquals(
|
||||
'',
|
||||
$this->parser->parse('[not_shortcode]')
|
||||
);
|
||||
|
||||
ShortcodeParser::$error_behavior = ShortcodeParser::WARN;
|
||||
$this->assertEquals(
|
||||
'<strong class="warning">[not_shortcode]</strong>',
|
||||
$this->parser->parse('[not_shortcode]')
|
||||
);
|
||||
|
||||
ShortcodeParser::$error_behavior = ShortcodeParser::LEAVE;
|
||||
$this->assertEquals('[not_shortcode]',
|
||||
$this->parser->parse('[not_shortcode]'));
|
||||
$this->assertEquals('[not_shortcode /]',
|
||||
@ -26,11 +39,16 @@ class ShortcodeParserTest extends SapphireTest {
|
||||
$this->parser->parse('[not_shortcode,foo="bar"]'));
|
||||
$this->assertEquals('[not_shortcode]a[/not_shortcode]',
|
||||
$this->parser->parse('[not_shortcode]a[/not_shortcode]'));
|
||||
$this->assertEquals('[/not_shortcode]',
|
||||
$this->parser->parse('[/not_shortcode]'));
|
||||
}
|
||||
|
||||
public function testSimpleTag() {
|
||||
$tests = array('[test_shortcode]', '[test_shortcode ]', '[test_shortcode,]', '[test_shortcode/]',
|
||||
'[test_shortcode /]');
|
||||
$tests = array(
|
||||
'[test_shortcode]',
|
||||
'[test_shortcode ]', '[test_shortcode,]', '[test_shortcode, ]'.
|
||||
'[test_shortcode/]', '[test_shortcode /]', '[test_shortcode,/]', '[test_shortcode, /]'
|
||||
);
|
||||
|
||||
foreach($tests as $test) {
|
||||
$this->parser->parse($test);
|
||||
@ -43,9 +61,9 @@ class ShortcodeParserTest extends SapphireTest {
|
||||
|
||||
public function testOneArgument() {
|
||||
$tests = array (
|
||||
'[test_shortcode,foo="bar"]',
|
||||
"[test_shortcode,foo='bar']",
|
||||
'[test_shortcode,foo = "bar" /]'
|
||||
'[test_shortcode foo="bar"]', '[test_shortcode,foo="bar"]',
|
||||
"[test_shortcode foo='bar']", "[test_shortcode,foo='bar']",
|
||||
'[test_shortcode foo = "bar" /]', '[test_shortcode, foo = "bar" /]'
|
||||
);
|
||||
|
||||
foreach($tests as $test) {
|
||||
@ -58,7 +76,7 @@ class ShortcodeParserTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testMultipleArguments() {
|
||||
$this->parser->parse('[test_shortcode,foo = "bar",bar=\'foo\',baz="buz"]');
|
||||
$this->parser->parse('[test_shortcode foo = "bar",bar=\'foo\', baz="buz"]');
|
||||
|
||||
$this->assertEquals(array('foo' => 'bar', 'bar' => 'foo', 'baz' => 'buz'), $this->arguments);
|
||||
$this->assertEquals('', $this->contents);
|
||||
@ -86,7 +104,7 @@ class ShortcodeParserTest extends SapphireTest {
|
||||
$this->assertEquals('[test_shortcode]content[/test_shortcode]',
|
||||
$this->parser->parse('[[test_shortcode]content[/test_shortcode]]'));
|
||||
}
|
||||
|
||||
|
||||
public function testUnquotedArguments() {
|
||||
$this->assertEquals('', $this->parser->parse('[test_shortcode,foo=bar,baz = buz]'));
|
||||
$this->assertEquals(array('foo' => 'bar', 'baz' => 'buz'), $this->arguments);
|
||||
@ -111,6 +129,42 @@ class ShortcodeParserTest extends SapphireTest {
|
||||
$this->assertEquals('', $this->parser->parse('[test_shortcode][test_shortcode]'));
|
||||
}
|
||||
|
||||
protected function assertEqualsIgnoringWhitespace($a, $b, $message = null) {
|
||||
$this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b), $message);
|
||||
}
|
||||
|
||||
public function testtExtract() {
|
||||
// Left extracts to before the current block
|
||||
$this->assertEqualsIgnoringWhitespace(
|
||||
'Code<div>FooBar</div>',
|
||||
$this->parser->parse('<div>Foo[test_shortcode class=left]Code[/test_shortcode]Bar</div>')
|
||||
);
|
||||
|
||||
// Even if the immediate parent isn't a the current block
|
||||
$this->assertEqualsIgnoringWhitespace(
|
||||
'Code<div>Foo<b>BarBaz</b>Qux</div>',
|
||||
$this->parser->parse('<div>Foo<b>Bar[test_shortcode class=left]Code[/test_shortcode]Baz</b>Qux</div>')
|
||||
);
|
||||
|
||||
// Center splits the current block
|
||||
$this->assertEqualsIgnoringWhitespace(
|
||||
'<div>Foo</div>Code<div>Bar</div>',
|
||||
$this->parser->parse('<div>Foo[test_shortcode class=center]Code[/test_shortcode]Bar</div>')
|
||||
);
|
||||
|
||||
// Even if the immediate parent isn't a the current block
|
||||
$this->assertEqualsIgnoringWhitespace(
|
||||
'<div>Foo<b>Bar</b></div>Code<div><b>Baz</b>Qux</div>',
|
||||
$this->parser->parse('<div>Foo<b>Bar[test_shortcode class=center]Code[/test_shortcode]Baz</b>Qux</div>')
|
||||
);
|
||||
|
||||
// No class means don't extract
|
||||
$this->assertEqualsIgnoringWhitespace(
|
||||
'<div>FooCodeBar</div>',
|
||||
$this->parser->parse('<div>Foo[test_shortcode]Code[/test_shortcode]Bar</div>')
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
@ -114,6 +114,23 @@ class MemberTest extends FunctionalTest {
|
||||
|
||||
Security::set_password_encryption_algorithm($origAlgo);
|
||||
}
|
||||
|
||||
public function testKeepsEncryptionOnEmptyPasswords() {
|
||||
$member = new Member();
|
||||
$member->Password = 'mypassword';
|
||||
$member->PasswordEncryption = 'sha1_v2.4';
|
||||
$member->write();
|
||||
|
||||
$member->Password = '';
|
||||
$member->write();
|
||||
|
||||
$this->assertEquals(
|
||||
$member->PasswordEncryption,
|
||||
'sha1_v2.4'
|
||||
);
|
||||
$result = $member->checkPassword('');
|
||||
$this->assertTrue($result->valid());
|
||||
}
|
||||
|
||||
public function testSetPassword() {
|
||||
$member = $this->objFromFixture('Member', 'test');
|
||||
|
@ -57,7 +57,10 @@ class SecurityTest extends FunctionalTest {
|
||||
|
||||
$response = $this->get('SecurityTest_SecuredController');
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertContains('Security/login', $response->getHeader('Location'));
|
||||
$this->assertContains(
|
||||
Config::inst()->get('Security', 'login_url'),
|
||||
$response->getHeader('Location')
|
||||
);
|
||||
|
||||
$this->logInWithPermission('ADMIN');
|
||||
$response = $this->get('SecurityTest_SecuredController');
|
||||
@ -74,7 +77,7 @@ class SecurityTest extends FunctionalTest {
|
||||
$this->session()->inst_set('loggedInAs', $member->ID);
|
||||
|
||||
/* View the Security/login page */
|
||||
$response = $this->get('Security/login');
|
||||
$response = $this->get(Config::inst()->get('Security', 'login_url'));
|
||||
|
||||
$items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.action');
|
||||
|
||||
@ -108,7 +111,7 @@ class SecurityTest extends FunctionalTest {
|
||||
$this->autoFollowRedirection = true;
|
||||
|
||||
/* Attempt to get into the admin section */
|
||||
$response = $this->get('Security/login/');
|
||||
$response = $this->get(Config::inst()->get('Security', 'login_url'));
|
||||
|
||||
$items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.text');
|
||||
|
||||
@ -396,7 +399,7 @@ class SecurityTest extends FunctionalTest {
|
||||
public function doTestLoginForm($email, $password, $backURL = 'test/link') {
|
||||
$this->get('Security/logout');
|
||||
$this->session()->inst_set('BackURL', $backURL);
|
||||
$this->get('Security/login');
|
||||
$this->get(Config::inst()->get('Security', 'login_url'));
|
||||
|
||||
return $this->submitForm(
|
||||
"MemberLoginForm_LoginForm",
|
||||
|
1
tests/templates/SSViewerTestIncludeObjectArguments.ss
Normal file
1
tests/templates/SSViewerTestIncludeObjectArguments.ss
Normal file
@ -0,0 +1 @@
|
||||
$A.Key $B.Key
|
@ -15,8 +15,8 @@ echo ""
|
||||
# Fetch all dependencies
|
||||
# TODO Replace with different composer.json variations
|
||||
|
||||
echo "Checking out installer@$TRAVIS_BRANCH"
|
||||
git clone --depth=100 --quiet -b $TRAVIS_BRANCH git://github.com/silverstripe/silverstripe-installer.git $BUILD_DIR
|
||||
echo "Checking out installer@3.1"
|
||||
git clone --depth=100 --quiet -b 3.1 git://github.com/silverstripe/silverstripe-installer.git $BUILD_DIR
|
||||
|
||||
echo "Checking out sqlite3@master"
|
||||
git clone --depth=100 --quiet git://github.com/silverstripe-labs/silverstripe-sqlite3.git $BUILD_DIR/sqlite3
|
||||
|
@ -508,6 +508,17 @@ after')
|
||||
new ArrayData(array('Arg1' => 'Foo', 'Arg2' => 'Bar'))),
|
||||
'<p>A</p><p>Bar</p>'
|
||||
);
|
||||
|
||||
$data = new ArrayData(array(
|
||||
'Nested' => new ArrayData(array(
|
||||
'Object' => new ArrayData(array('Key' => 'A'))
|
||||
)),
|
||||
'Object' => new ArrayData(array('Key' => 'B'))
|
||||
));
|
||||
|
||||
$tmpl = SSViewer::fromString('<% include SSViewerTestIncludeObjectArguments A=$Nested.Object, B=$Object %>');
|
||||
$res = $tmpl->process($data);
|
||||
$this->assertEqualIgnoringWhitespace('A B', $res, 'Objects can be passed as named arguments');
|
||||
}
|
||||
|
||||
|
||||
|
114
thirdparty/html5lib/HTML5/Data.php
vendored
Normal file
114
thirdparty/html5lib/HTML5/Data.php
vendored
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
// warning: this file is encoded in UTF-8!
|
||||
|
||||
class HTML5_Data
|
||||
{
|
||||
|
||||
// at some point this should be moved to a .ser file. Another
|
||||
// possible optimization is to give UTF-8 bytes, not Unicode
|
||||
// codepoints
|
||||
// XXX: Not quite sure why it's named this; this is
|
||||
// actually the numeric entity dereference table.
|
||||
protected static $realCodepointTable = array(
|
||||
0x00 => 0xFFFD, // REPLACEMENT CHARACTER
|
||||
0x0D => 0x000A, // LINE FEED (LF)
|
||||
0x80 => 0x20AC, // EURO SIGN ('€')
|
||||
0x81 => 0x0081, // <control>
|
||||
0x82 => 0x201A, // SINGLE LOW-9 QUOTATION MARK ('‚')
|
||||
0x83 => 0x0192, // LATIN SMALL LETTER F WITH HOOK ('ƒ')
|
||||
0x84 => 0x201E, // DOUBLE LOW-9 QUOTATION MARK ('„')
|
||||
0x85 => 0x2026, // HORIZONTAL ELLIPSIS ('…')
|
||||
0x86 => 0x2020, // DAGGER ('†')
|
||||
0x87 => 0x2021, // DOUBLE DAGGER ('‡')
|
||||
0x88 => 0x02C6, // MODIFIER LETTER CIRCUMFLEX ACCENT ('ˆ')
|
||||
0x89 => 0x2030, // PER MILLE SIGN ('‰')
|
||||
0x8A => 0x0160, // LATIN CAPITAL LETTER S WITH CARON ('Š')
|
||||
0x8B => 0x2039, // SINGLE LEFT-POINTING ANGLE QUOTATION MARK ('‹')
|
||||
0x8C => 0x0152, // LATIN CAPITAL LIGATURE OE ('Œ')
|
||||
0x8D => 0x008D, // <control>
|
||||
0x8E => 0x017D, // LATIN CAPITAL LETTER Z WITH CARON ('Ž')
|
||||
0x8F => 0x008F, // <control>
|
||||
0x90 => 0x0090, // <control>
|
||||
0x91 => 0x2018, // LEFT SINGLE QUOTATION MARK ('‘')
|
||||
0x92 => 0x2019, // RIGHT SINGLE QUOTATION MARK ('’')
|
||||
0x93 => 0x201C, // LEFT DOUBLE QUOTATION MARK ('“')
|
||||
0x94 => 0x201D, // RIGHT DOUBLE QUOTATION MARK ('”')
|
||||
0x95 => 0x2022, // BULLET ('•')
|
||||
0x96 => 0x2013, // EN DASH ('–')
|
||||
0x97 => 0x2014, // EM DASH ('—')
|
||||
0x98 => 0x02DC, // SMALL TILDE ('˜')
|
||||
0x99 => 0x2122, // TRADE MARK SIGN ('™')
|
||||
0x9A => 0x0161, // LATIN SMALL LETTER S WITH CARON ('š')
|
||||
0x9B => 0x203A, // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK ('›')
|
||||
0x9C => 0x0153, // LATIN SMALL LIGATURE OE ('œ')
|
||||
0x9D => 0x009D, // <control>
|
||||
0x9E => 0x017E, // LATIN SMALL LETTER Z WITH CARON ('ž')
|
||||
0x9F => 0x0178, // LATIN CAPITAL LETTER Y WITH DIAERESIS ('Ÿ')
|
||||
);
|
||||
|
||||
protected static $namedCharacterReferences;
|
||||
|
||||
protected static $namedCharacterReferenceMaxLength;
|
||||
|
||||
/**
|
||||
* Returns the "real" Unicode codepoint of a malformed character
|
||||
* reference.
|
||||
*/
|
||||
public static function getRealCodepoint($ref) {
|
||||
if (!isset(self::$realCodepointTable[$ref])) return false;
|
||||
else return self::$realCodepointTable[$ref];
|
||||
}
|
||||
|
||||
public static function getNamedCharacterReferences() {
|
||||
if (!self::$namedCharacterReferences) {
|
||||
self::$namedCharacterReferences = unserialize(
|
||||
file_get_contents(dirname(__FILE__) . '/named-character-references.ser'));
|
||||
}
|
||||
return self::$namedCharacterReferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Unicode codepoint to sequence of UTF-8 bytes.
|
||||
* @note Shamelessly stolen from HTML Purifier, which is also
|
||||
* shamelessly stolen from Feyd (which is in public domain).
|
||||
*/
|
||||
public static function utf8chr($code) {
|
||||
/* We don't care: we live dangerously
|
||||
* if($code > 0x10FFFF or $code < 0x0 or
|
||||
($code >= 0xD800 and $code <= 0xDFFF) ) {
|
||||
// bits are set outside the "valid" range as defined
|
||||
// by UNICODE 4.1.0
|
||||
return "\xEF\xBF\xBD";
|
||||
}*/
|
||||
|
||||
$x = $y = $z = $w = 0;
|
||||
if ($code < 0x80) {
|
||||
// regular ASCII character
|
||||
$x = $code;
|
||||
} else {
|
||||
// set up bits for UTF-8
|
||||
$x = ($code & 0x3F) | 0x80;
|
||||
if ($code < 0x800) {
|
||||
$y = (($code & 0x7FF) >> 6) | 0xC0;
|
||||
} else {
|
||||
$y = (($code & 0xFC0) >> 6) | 0x80;
|
||||
if($code < 0x10000) {
|
||||
$z = (($code >> 12) & 0x0F) | 0xE0;
|
||||
} else {
|
||||
$z = (($code >> 12) & 0x3F) | 0x80;
|
||||
$w = (($code >> 18) & 0x07) | 0xF0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// set up the actual character
|
||||
$ret = '';
|
||||
if($w) $ret .= chr($w);
|
||||
if($z) $ret .= chr($z);
|
||||
if($y) $ret .= chr($y);
|
||||
$ret .= chr($x);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
}
|
284
thirdparty/html5lib/HTML5/InputStream.php
vendored
Normal file
284
thirdparty/html5lib/HTML5/InputStream.php
vendored
Normal file
@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright 2009 Geoffrey Sneddon <http://gsnedders.com/>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
// Some conventions:
|
||||
// /* */ indicates verbatim text from the HTML 5 specification
|
||||
// // indicates regular comments
|
||||
|
||||
class HTML5_InputStream {
|
||||
/**
|
||||
* The string data we're parsing.
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* The current integer byte position we are in $data
|
||||
*/
|
||||
private $char;
|
||||
|
||||
/**
|
||||
* Length of $data; when $char === $data, we are at the end-of-file.
|
||||
*/
|
||||
private $EOF;
|
||||
|
||||
/**
|
||||
* Parse errors.
|
||||
*/
|
||||
public $errors = array();
|
||||
|
||||
/**
|
||||
* @param $data Data to parse
|
||||
*/
|
||||
public function __construct($data) {
|
||||
|
||||
/* Given an encoding, the bytes in the input stream must be
|
||||
converted to Unicode characters for the tokeniser, as
|
||||
described by the rules for that encoding, except that the
|
||||
leading U+FEFF BYTE ORDER MARK character, if any, must not
|
||||
be stripped by the encoding layer (it is stripped by the rule below).
|
||||
|
||||
Bytes or sequences of bytes in the original byte stream that
|
||||
could not be converted to Unicode characters must be converted
|
||||
to U+FFFD REPLACEMENT CHARACTER code points. */
|
||||
|
||||
// XXX currently assuming input data is UTF-8; once we
|
||||
// build encoding detection this will no longer be the case
|
||||
//
|
||||
// We previously had an mbstring implementation here, but that
|
||||
// implementation is heavily non-conforming, so it's been
|
||||
// omitted.
|
||||
if (extension_loaded('iconv')) {
|
||||
// non-conforming
|
||||
$data = @iconv('UTF-8', 'UTF-8//IGNORE', $data);
|
||||
} else {
|
||||
// we can make a conforming native implementation
|
||||
throw new Exception('Not implemented, please install mbstring or iconv');
|
||||
}
|
||||
|
||||
/* One leading U+FEFF BYTE ORDER MARK character must be
|
||||
ignored if any are present. */
|
||||
if (substr($data, 0, 3) === "\xEF\xBB\xBF") {
|
||||
$data = substr($data, 3);
|
||||
}
|
||||
|
||||
/* All U+0000 NULL characters in the input must be replaced
|
||||
by U+FFFD REPLACEMENT CHARACTERs. Any occurrences of such
|
||||
characters is a parse error. */
|
||||
for ($i = 0, $count = substr_count($data, "\0"); $i < $count; $i++) {
|
||||
$this->errors[] = array(
|
||||
'type' => HTML5_Tokenizer::PARSEERROR,
|
||||
'data' => 'null-character'
|
||||
);
|
||||
}
|
||||
/* U+000D CARRIAGE RETURN (CR) characters and U+000A LINE FEED
|
||||
(LF) characters are treated specially. Any CR characters
|
||||
that are followed by LF characters must be removed, and any
|
||||
CR characters not followed by LF characters must be converted
|
||||
to LF characters. Thus, newlines in HTML DOMs are represented
|
||||
by LF characters, and there are never any CR characters in the
|
||||
input to the tokenization stage. */
|
||||
$data = str_replace(
|
||||
array(
|
||||
"\0",
|
||||
"\r\n",
|
||||
"\r"
|
||||
),
|
||||
array(
|
||||
"\xEF\xBF\xBD",
|
||||
"\n",
|
||||
"\n"
|
||||
),
|
||||
$data
|
||||
);
|
||||
|
||||
/* Any occurrences of any characters in the ranges U+0001 to
|
||||
U+0008, U+000B, U+000E to U+001F, U+007F to U+009F,
|
||||
U+D800 to U+DFFF , U+FDD0 to U+FDEF, and
|
||||
characters U+FFFE, U+FFFF, U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF,
|
||||
U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE,
|
||||
U+6FFFF, U+7FFFE, U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF,
|
||||
U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF, U+DFFFE,
|
||||
U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, and
|
||||
U+10FFFF are parse errors. (These are all control characters
|
||||
or permanently undefined Unicode characters.) */
|
||||
// Check PCRE is loaded.
|
||||
if (extension_loaded('pcre')) {
|
||||
$count = preg_match_all(
|
||||
'/(?:
|
||||
[\x01-\x08\x0B\x0E-\x1F\x7F] # U+0001 to U+0008, U+000B, U+000E to U+001F and U+007F
|
||||
|
|
||||
\xC2[\x80-\x9F] # U+0080 to U+009F
|
||||
|
|
||||
\xED(?:\xA0[\x80-\xFF]|[\xA1-\xBE][\x00-\xFF]|\xBF[\x00-\xBF]) # U+D800 to U+DFFFF
|
||||
|
|
||||
\xEF\xB7[\x90-\xAF] # U+FDD0 to U+FDEF
|
||||
|
|
||||
\xEF\xBF[\xBE\xBF] # U+FFFE and U+FFFF
|
||||
|
|
||||
[\xF0-\xF4][\x8F-\xBF]\xBF[\xBE\xBF] # U+nFFFE and U+nFFFF (1 <= n <= 10_{16})
|
||||
)/x',
|
||||
$data,
|
||||
$matches
|
||||
);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$this->errors[] = array(
|
||||
'type' => HTML5_Tokenizer::PARSEERROR,
|
||||
'data' => 'invalid-codepoint'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// XXX: Need non-PCRE impl, probably using substr_count
|
||||
}
|
||||
|
||||
$this->data = $data;
|
||||
$this->char = 0;
|
||||
$this->EOF = strlen($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current line that the tokenizer is at.
|
||||
*/
|
||||
public function getCurrentLine() {
|
||||
// Check the string isn't empty
|
||||
if($this->EOF) {
|
||||
// Add one to $this->char because we want the number for the next
|
||||
// byte to be processed.
|
||||
return substr_count($this->data, "\n", 0, min($this->char, $this->EOF)) + 1;
|
||||
} else {
|
||||
// If the string is empty, we are on the first line (sorta).
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current column of the current line that the tokenizer is at.
|
||||
*/
|
||||
public function getColumnOffset() {
|
||||
// strrpos is weird, and the offset needs to be negative for what we
|
||||
// want (i.e., the last \n before $this->char). This needs to not have
|
||||
// one (to make it point to the next character, the one we want the
|
||||
// position of) added to it because strrpos's behaviour includes the
|
||||
// final offset byte.
|
||||
$lastLine = strrpos($this->data, "\n", $this->char - 1 - strlen($this->data));
|
||||
|
||||
// However, for here we want the length up until the next byte to be
|
||||
// processed, so add one to the current byte ($this->char).
|
||||
if($lastLine !== false) {
|
||||
$findLengthOf = substr($this->data, $lastLine + 1, $this->char - 1 - $lastLine);
|
||||
} else {
|
||||
$findLengthOf = substr($this->data, 0, $this->char);
|
||||
}
|
||||
|
||||
// Get the length for the string we need.
|
||||
if(extension_loaded('iconv')) {
|
||||
return iconv_strlen($findLengthOf, 'utf-8');
|
||||
} elseif(extension_loaded('mbstring')) {
|
||||
return mb_strlen($findLengthOf, 'utf-8');
|
||||
} elseif(extension_loaded('xml')) {
|
||||
return strlen(utf8_decode($findLengthOf));
|
||||
} else {
|
||||
$count = count_chars($findLengthOf);
|
||||
// 0x80 = 0x7F - 0 + 1 (one added to get inclusive range)
|
||||
// 0x33 = 0xF4 - 0x2C + 1 (one added to get inclusive range)
|
||||
return array_sum(array_slice($count, 0, 0x80)) +
|
||||
array_sum(array_slice($count, 0xC2, 0x33));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the currently consume character.
|
||||
* @note This performs bounds checking
|
||||
*/
|
||||
public function char() {
|
||||
return ($this->char++ < $this->EOF)
|
||||
? $this->data[$this->char - 1]
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all characters until EOF.
|
||||
* @note This performs bounds checking
|
||||
*/
|
||||
public function remainingChars() {
|
||||
if($this->char < $this->EOF) {
|
||||
$data = substr($this->data, $this->char);
|
||||
$this->char = $this->EOF;
|
||||
return $data;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches as far as possible until we reach a certain set of bytes
|
||||
* and returns the matched substring.
|
||||
* @param $bytes Bytes to match.
|
||||
*/
|
||||
public function charsUntil($bytes, $max = null) {
|
||||
if ($this->char < $this->EOF) {
|
||||
if ($max === 0 || $max) {
|
||||
$len = strcspn($this->data, $bytes, $this->char, $max);
|
||||
} else {
|
||||
$len = strcspn($this->data, $bytes, $this->char);
|
||||
}
|
||||
$string = (string) substr($this->data, $this->char, $len);
|
||||
$this->char += $len;
|
||||
return $string;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches as far as possible with a certain set of bytes
|
||||
* and returns the matched substring.
|
||||
* @param $bytes Bytes to match.
|
||||
*/
|
||||
public function charsWhile($bytes, $max = null) {
|
||||
if ($this->char < $this->EOF) {
|
||||
if ($max === 0 || $max) {
|
||||
$len = strspn($this->data, $bytes, $this->char, $max);
|
||||
} else {
|
||||
$len = strspn($this->data, $bytes, $this->char);
|
||||
}
|
||||
$string = (string) substr($this->data, $this->char, $len);
|
||||
$this->char += $len;
|
||||
return $string;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unconsume one character.
|
||||
*/
|
||||
public function unget() {
|
||||
if ($this->char <= $this->EOF) {
|
||||
$this->char--;
|
||||
}
|
||||
}
|
||||
}
|
36
thirdparty/html5lib/HTML5/Parser.php
vendored
Normal file
36
thirdparty/html5lib/HTML5/Parser.php
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__) . '/Data.php';
|
||||
require_once dirname(__FILE__) . '/InputStream.php';
|
||||
require_once dirname(__FILE__) . '/TreeBuilder.php';
|
||||
require_once dirname(__FILE__) . '/Tokenizer.php';
|
||||
|
||||
/**
|
||||
* Outwards facing interface for HTML5.
|
||||
*/
|
||||
class HTML5_Parser
|
||||
{
|
||||
/**
|
||||
* Parses a full HTML document.
|
||||
* @param $text HTML text to parse
|
||||
* @param $builder Custom builder implementation
|
||||
* @return Parsed HTML as DOMDocument
|
||||
*/
|
||||
static public function parse($text, $builder = null) {
|
||||
$tokenizer = new HTML5_Tokenizer($text, $builder);
|
||||
$tokenizer->parse();
|
||||
return $tokenizer->save();
|
||||
}
|
||||
/**
|
||||
* Parses an HTML fragment.
|
||||
* @param $text HTML text to parse
|
||||
* @param $context String name of context element to pretend parsing is in.
|
||||
* @param $builder Custom builder implementation
|
||||
* @return Parsed HTML as DOMDocument
|
||||
*/
|
||||
static public function parseFragment($text, $context = null, $builder = null) {
|
||||
$tokenizer = new HTML5_Tokenizer($text, $builder);
|
||||
$tokenizer->parseFragment($context);
|
||||
return $tokenizer->save();
|
||||
}
|
||||
}
|
2422
thirdparty/html5lib/HTML5/Tokenizer.php
vendored
Normal file
2422
thirdparty/html5lib/HTML5/Tokenizer.php
vendored
Normal file
@ -0,0 +1,2422 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright 2007 Jeroen van der Meer <http://jero.net/>
|
||||
Copyright 2008 Edward Z. Yang <http://htmlpurifier.org/>
|
||||
Copyright 2009 Geoffrey Sneddon <http://gsnedders.com/>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
// Some conventions:
|
||||
// /* */ indicates verbatim text from the HTML 5 specification
|
||||
// // indicates regular comments
|
||||
|
||||
// all flags are in hyphenated form
|
||||
|
||||
class HTML5_Tokenizer {
|
||||
/**
|
||||
* Points to an InputStream object.
|
||||
*/
|
||||
protected $stream;
|
||||
|
||||
/**
|
||||
* Tree builder that the tokenizer emits token to.
|
||||
*/
|
||||
private $tree;
|
||||
|
||||
/**
|
||||
* Current content model we are parsing as.
|
||||
*/
|
||||
protected $content_model;
|
||||
|
||||
/**
|
||||
* Current token that is being built, but not yet emitted. Also
|
||||
* is the last token emitted, if applicable.
|
||||
*/
|
||||
protected $token;
|
||||
|
||||
// These are constants describing the content model
|
||||
const PCDATA = 0;
|
||||
const RCDATA = 1;
|
||||
const CDATA = 2;
|
||||
const PLAINTEXT = 3;
|
||||
|
||||
// These are constants describing tokens
|
||||
// XXX should probably be moved somewhere else, probably the
|
||||
// HTML5 class.
|
||||
const DOCTYPE = 0;
|
||||
const STARTTAG = 1;
|
||||
const ENDTAG = 2;
|
||||
const COMMENT = 3;
|
||||
const CHARACTER = 4;
|
||||
const SPACECHARACTER = 5;
|
||||
const EOF = 6;
|
||||
const PARSEERROR = 7;
|
||||
|
||||
// These are constants representing bunches of characters.
|
||||
const ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
const UPPER_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const LOWER_ALPHA = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const DIGIT = '0123456789';
|
||||
const HEX = '0123456789ABCDEFabcdef';
|
||||
const WHITESPACE = "\t\n\x0c ";
|
||||
|
||||
/**
|
||||
* @param $data Data to parse
|
||||
*/
|
||||
public function __construct($data, $builder = null) {
|
||||
$this->stream = new HTML5_InputStream($data);
|
||||
if (!$builder) $this->tree = new HTML5_TreeBuilder;
|
||||
else $this->tree = $builder;
|
||||
$this->content_model = self::PCDATA;
|
||||
}
|
||||
|
||||
public function parseFragment($context = null) {
|
||||
$this->tree->setupContext($context);
|
||||
if ($this->tree->content_model) {
|
||||
$this->content_model = $this->tree->content_model;
|
||||
$this->tree->content_model = null;
|
||||
}
|
||||
$this->parse();
|
||||
}
|
||||
|
||||
// XXX maybe convert this into an iterator? regardless, this function
|
||||
// and the save function should go into a Parser facade of some sort
|
||||
/**
|
||||
* Performs the actual parsing of the document.
|
||||
*/
|
||||
public function parse() {
|
||||
// Current state
|
||||
$state = 'data';
|
||||
// This is used to avoid having to have look-behind in the data state.
|
||||
$lastFourChars = '';
|
||||
/**
|
||||
* Escape flag as specified by the HTML5 specification: "used to
|
||||
* control the behavior of the tokeniser. It is either true or
|
||||
* false, and initially must be set to the false state."
|
||||
*/
|
||||
$escape = false;
|
||||
//echo "\n\n";
|
||||
while($state !== null) {
|
||||
|
||||
/*echo $state . ' ';
|
||||
switch ($this->content_model) {
|
||||
case self::PCDATA: echo 'PCDATA'; break;
|
||||
case self::RCDATA: echo 'RCDATA'; break;
|
||||
case self::CDATA: echo 'CDATA'; break;
|
||||
case self::PLAINTEXT: echo 'PLAINTEXT'; break;
|
||||
}
|
||||
if ($escape) echo " escape";
|
||||
echo "\n";*/
|
||||
|
||||
switch($state) {
|
||||
case 'data':
|
||||
|
||||
/* Consume the next input character */
|
||||
$char = $this->stream->char();
|
||||
$lastFourChars .= $char;
|
||||
if (strlen($lastFourChars) > 4) $lastFourChars = substr($lastFourChars, -4);
|
||||
|
||||
// see below for meaning
|
||||
$hyp_cond =
|
||||
!$escape &&
|
||||
(
|
||||
$this->content_model === self::RCDATA ||
|
||||
$this->content_model === self::CDATA
|
||||
);
|
||||
$amp_cond =
|
||||
!$escape &&
|
||||
(
|
||||
$this->content_model === self::PCDATA ||
|
||||
$this->content_model === self::RCDATA
|
||||
);
|
||||
$lt_cond =
|
||||
$this->content_model === self::PCDATA ||
|
||||
(
|
||||
(
|
||||
$this->content_model === self::RCDATA ||
|
||||
$this->content_model === self::CDATA
|
||||
) &&
|
||||
!$escape
|
||||
);
|
||||
$gt_cond =
|
||||
$escape &&
|
||||
(
|
||||
$this->content_model === self::RCDATA ||
|
||||
$this->content_model === self::CDATA
|
||||
);
|
||||
|
||||
if($char === '&' && $amp_cond) {
|
||||
/* U+0026 AMPERSAND (&)
|
||||
When the content model flag is set to one of the PCDATA or RCDATA
|
||||
states and the escape flag is false: switch to the
|
||||
character reference data state. Otherwise: treat it as per
|
||||
the "anything else" entry below. */
|
||||
$state = 'character reference data';
|
||||
|
||||
} elseif(
|
||||
$char === '-' &&
|
||||
$hyp_cond &&
|
||||
$lastFourChars === '<!--'
|
||||
) {
|
||||
/*
|
||||
U+002D HYPHEN-MINUS (-)
|
||||
If the content model flag is set to either the RCDATA state or
|
||||
the CDATA state, and the escape flag is false, and there are at
|
||||
least three characters before this one in the input stream, and the
|
||||
last four characters in the input stream, including this one, are
|
||||
U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS,
|
||||
and U+002D HYPHEN-MINUS ("<!--"), then set the escape flag to true. */
|
||||
$escape = true;
|
||||
|
||||
/* In any case, emit the input character as a character token. Stay
|
||||
in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::CHARACTER,
|
||||
'data' => '-'
|
||||
));
|
||||
// We do the "any case" part as part of "anything else".
|
||||
|
||||
/* U+003C LESS-THAN SIGN (<) */
|
||||
} elseif($char === '<' && $lt_cond) {
|
||||
/* When the content model flag is set to the PCDATA state: switch
|
||||
to the tag open state.
|
||||
|
||||
When the content model flag is set to either the RCDATA state or
|
||||
the CDATA state and the escape flag is false: switch to the tag
|
||||
open state.
|
||||
|
||||
Otherwise: treat it as per the "anything else" entry below. */
|
||||
$state = 'tag open';
|
||||
|
||||
/* U+003E GREATER-THAN SIGN (>) */
|
||||
} elseif(
|
||||
$char === '>' &&
|
||||
$gt_cond &&
|
||||
substr($lastFourChars, 1) === '-->'
|
||||
) {
|
||||
/* If the content model flag is set to either the RCDATA state or
|
||||
the CDATA state, and the escape flag is true, and the last three
|
||||
characters in the input stream including this one are U+002D
|
||||
HYPHEN-MINUS, U+002D HYPHEN-MINUS, U+003E GREATER-THAN SIGN ("-->"),
|
||||
set the escape flag to false. */
|
||||
$escape = false;
|
||||
|
||||
/* In any case, emit the input character as a character token.
|
||||
Stay in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::CHARACTER,
|
||||
'data' => '>'
|
||||
));
|
||||
// We do the "any case" part as part of "anything else".
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Emit an end-of-file token. */
|
||||
$state = null;
|
||||
$this->tree->emitToken(array(
|
||||
'type' => self::EOF
|
||||
));
|
||||
|
||||
} elseif($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
// Directly after emitting a token you switch back to the "data
|
||||
// state". At that point spaceCharacters are important so they are
|
||||
// emitted separately.
|
||||
$chars = $this->stream->charsWhile(self::WHITESPACE);
|
||||
$this->emitToken(array(
|
||||
'type' => self::SPACECHARACTER,
|
||||
'data' => $char . $chars
|
||||
));
|
||||
$lastFourChars .= $chars;
|
||||
if (strlen($lastFourChars) > 4) $lastFourChars = substr($lastFourChars, -4);
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
THIS IS AN OPTIMIZATION: Get as many character that
|
||||
otherwise would also be treated as a character token and emit it
|
||||
as a single character token. Stay in the data state. */
|
||||
|
||||
$mask = '';
|
||||
if ($hyp_cond) $mask .= '-';
|
||||
if ($amp_cond) $mask .= '&';
|
||||
if ($lt_cond) $mask .= '<';
|
||||
if ($gt_cond) $mask .= '>';
|
||||
|
||||
if ($mask === '') {
|
||||
$chars = $this->stream->remainingChars();
|
||||
} else {
|
||||
$chars = $this->stream->charsUntil($mask);
|
||||
}
|
||||
|
||||
$this->emitToken(array(
|
||||
'type' => self::CHARACTER,
|
||||
'data' => $char . $chars
|
||||
));
|
||||
|
||||
$lastFourChars .= $chars;
|
||||
if (strlen($lastFourChars) > 4) $lastFourChars = substr($lastFourChars, -4);
|
||||
|
||||
$state = 'data';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'character reference data':
|
||||
/* (This cannot happen if the content model flag
|
||||
is set to the CDATA state.) */
|
||||
|
||||
/* Attempt to consume a character reference, with no
|
||||
additional allowed character. */
|
||||
$entity = $this->consumeCharacterReference();
|
||||
|
||||
/* If nothing is returned, emit a U+0026 AMPERSAND
|
||||
character token. Otherwise, emit the character token that
|
||||
was returned. */
|
||||
// This is all done when consuming the character reference.
|
||||
$this->emitToken(array(
|
||||
'type' => self::CHARACTER,
|
||||
'data' => $entity
|
||||
));
|
||||
|
||||
/* Finally, switch to the data state. */
|
||||
$state = 'data';
|
||||
break;
|
||||
|
||||
case 'tag open':
|
||||
$char = $this->stream->char();
|
||||
|
||||
switch($this->content_model) {
|
||||
case self::RCDATA:
|
||||
case self::CDATA:
|
||||
/* Consume the next input character. If it is a
|
||||
U+002F SOLIDUS (/) character, switch to the close
|
||||
tag open state. Otherwise, emit a U+003C LESS-THAN
|
||||
SIGN character token and reconsume the current input
|
||||
character in the data state. */
|
||||
// We consumed above.
|
||||
|
||||
if($char === '/') {
|
||||
$state = 'close tag open';
|
||||
|
||||
} else {
|
||||
$this->emitToken(array(
|
||||
'type' => self::CHARACTER,
|
||||
'data' => '<'
|
||||
));
|
||||
|
||||
$this->stream->unget();
|
||||
|
||||
$state = 'data';
|
||||
}
|
||||
break;
|
||||
|
||||
case self::PCDATA:
|
||||
/* If the content model flag is set to the PCDATA state
|
||||
Consume the next input character: */
|
||||
// We consumed above.
|
||||
|
||||
if($char === '!') {
|
||||
/* U+0021 EXCLAMATION MARK (!)
|
||||
Switch to the markup declaration open state. */
|
||||
$state = 'markup declaration open';
|
||||
|
||||
} elseif($char === '/') {
|
||||
/* U+002F SOLIDUS (/)
|
||||
Switch to the close tag open state. */
|
||||
$state = 'close tag open';
|
||||
|
||||
} elseif('A' <= $char && $char <= 'Z') {
|
||||
/* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
|
||||
Create a new start tag token, set its tag name to the lowercase
|
||||
version of the input character (add 0x0020 to the character's code
|
||||
point), then switch to the tag name state. (Don't emit the token
|
||||
yet; further details will be filled in before it is emitted.) */
|
||||
$this->token = array(
|
||||
'name' => strtolower($char),
|
||||
'type' => self::STARTTAG,
|
||||
'attr' => array()
|
||||
);
|
||||
|
||||
$state = 'tag name';
|
||||
|
||||
} elseif('a' <= $char && $char <= 'z') {
|
||||
/* U+0061 LATIN SMALL LETTER A through to U+007A LATIN SMALL LETTER Z
|
||||
Create a new start tag token, set its tag name to the input
|
||||
character, then switch to the tag name state. (Don't emit
|
||||
the token yet; further details will be filled in before it
|
||||
is emitted.) */
|
||||
$this->token = array(
|
||||
'name' => $char,
|
||||
'type' => self::STARTTAG,
|
||||
'attr' => array()
|
||||
);
|
||||
|
||||
$state = 'tag name';
|
||||
|
||||
} elseif($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Parse error. Emit a U+003C LESS-THAN SIGN character token and a
|
||||
U+003E GREATER-THAN SIGN character token. Switch to the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-tag-name-but-got-right-bracket'
|
||||
));
|
||||
$this->emitToken(array(
|
||||
'type' => self::CHARACTER,
|
||||
'data' => '<>'
|
||||
));
|
||||
|
||||
$state = 'data';
|
||||
|
||||
} elseif($char === '?') {
|
||||
/* U+003F QUESTION MARK (?)
|
||||
Parse error. Switch to the bogus comment state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-tag-name-but-got-question-mark'
|
||||
));
|
||||
$this->token = array(
|
||||
'data' => '?',
|
||||
'type' => self::COMMENT
|
||||
);
|
||||
$state = 'bogus comment';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Parse error. Emit a U+003C LESS-THAN SIGN character token and
|
||||
reconsume the current input character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-tag-name'
|
||||
));
|
||||
$this->emitToken(array(
|
||||
'type' => self::CHARACTER,
|
||||
'data' => '<'
|
||||
));
|
||||
|
||||
$state = 'data';
|
||||
$this->stream->unget();
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'close tag open':
|
||||
if (
|
||||
$this->content_model === self::RCDATA ||
|
||||
$this->content_model === self::CDATA
|
||||
) {
|
||||
/* If the content model flag is set to the RCDATA or CDATA
|
||||
states... */
|
||||
$name = strtolower($this->stream->charsWhile(self::ALPHA));
|
||||
$following = $this->stream->char();
|
||||
$this->stream->unget();
|
||||
if (
|
||||
!$this->token ||
|
||||
$this->token['name'] !== $name ||
|
||||
$this->token['name'] === $name && !in_array($following, array("\x09", "\x0A", "\x0C", "\x20", "\x3E", "\x2F", false))
|
||||
) {
|
||||
/* if no start tag token has ever been emitted by this instance
|
||||
of the tokenizer (fragment case), or, if the next few
|
||||
characters do not match the tag name of the last start tag
|
||||
token emitted (compared in an ASCII case-insensitive manner),
|
||||
or if they do but they are not immediately followed by one of
|
||||
the following characters:
|
||||
|
||||
* U+0009 CHARACTER TABULATION
|
||||
* U+000A LINE FEED (LF)
|
||||
* U+000C FORM FEED (FF)
|
||||
* U+0020 SPACE
|
||||
* U+003E GREATER-THAN SIGN (>)
|
||||
* U+002F SOLIDUS (/)
|
||||
* EOF
|
||||
|
||||
...then emit a U+003C LESS-THAN SIGN character token, a
|
||||
U+002F SOLIDUS character token, and switch to the data
|
||||
state to process the next input character. */
|
||||
// XXX: Probably ought to replace in_array with $following === x ||...
|
||||
|
||||
// We also need to emit $name now we've consumed that, as we
|
||||
// know it'll just be emitted as a character token.
|
||||
$this->emitToken(array(
|
||||
'type' => self::CHARACTER,
|
||||
'data' => '</' . $name
|
||||
));
|
||||
|
||||
$state = 'data';
|
||||
} else {
|
||||
// This matches what would happen if we actually did the
|
||||
// otherwise below (but we can't because we've consumed too
|
||||
// much).
|
||||
|
||||
// Start the end tag token with the name we already have.
|
||||
$this->token = array(
|
||||
'name' => $name,
|
||||
'type' => self::ENDTAG
|
||||
);
|
||||
|
||||
// Change to tag name state.
|
||||
$state = 'tag name';
|
||||
}
|
||||
} elseif ($this->content_model === self::PCDATA) {
|
||||
/* Otherwise, if the content model flag is set to the PCDATA
|
||||
state [...]: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if ('A' <= $char && $char <= 'Z') {
|
||||
/* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
|
||||
Create a new end tag token, set its tag name to the lowercase version
|
||||
of the input character (add 0x0020 to the character's code point), then
|
||||
switch to the tag name state. (Don't emit the token yet; further details
|
||||
will be filled in before it is emitted.) */
|
||||
$this->token = array(
|
||||
'name' => strtolower($char),
|
||||
'type' => self::ENDTAG
|
||||
);
|
||||
|
||||
$state = 'tag name';
|
||||
|
||||
} elseif ('a' <= $char && $char <= 'z') {
|
||||
/* U+0061 LATIN SMALL LETTER A through to U+007A LATIN SMALL LETTER Z
|
||||
Create a new end tag token, set its tag name to the
|
||||
input character, then switch to the tag name state.
|
||||
(Don't emit the token yet; further details will be
|
||||
filled in before it is emitted.) */
|
||||
$this->token = array(
|
||||
'name' => $char,
|
||||
'type' => self::ENDTAG
|
||||
);
|
||||
|
||||
$state = 'tag name';
|
||||
|
||||
} elseif($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Parse error. Switch to the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-closing-tag-but-got-right-bracket'
|
||||
));
|
||||
$state = 'data';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F
|
||||
SOLIDUS character token. Reconsume the EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-closing-tag-but-got-eof'
|
||||
));
|
||||
$this->emitToken(array(
|
||||
'type' => self::CHARACTER,
|
||||
'data' => '</'
|
||||
));
|
||||
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Parse error. Switch to the bogus comment state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-closing-tag-but-got-char'
|
||||
));
|
||||
$this->token = array(
|
||||
'data' => $char,
|
||||
'type' => self::COMMENT
|
||||
);
|
||||
$state = 'bogus comment';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tag name':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Switch to the before attribute name state. */
|
||||
$state = 'before attribute name';
|
||||
|
||||
} elseif($char === '/') {
|
||||
/* U+002F SOLIDUS (/)
|
||||
Switch to the self-closing start tag state. */
|
||||
$state = 'self-closing start tag';
|
||||
|
||||
} elseif($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Emit the current tag token. Switch to the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
|
||||
} elseif('A' <= $char && $char <= 'Z') {
|
||||
/* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z
|
||||
Append the lowercase version of the current input
|
||||
character (add 0x0020 to the character's code point) to
|
||||
the current tag token's tag name. Stay in the tag name state. */
|
||||
$chars = $this->stream->charsWhile(self::UPPER_ALPHA);
|
||||
|
||||
$this->token['name'] .= strtolower($char . $chars);
|
||||
$state = 'tag name';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Reconsume the EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-tag-name'
|
||||
));
|
||||
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Append the current input character to the current tag token's tag name.
|
||||
Stay in the tag name state. */
|
||||
$chars = $this->stream->charsUntil("\t\n\x0C />" . self::UPPER_ALPHA);
|
||||
|
||||
$this->token['name'] .= $char . $chars;
|
||||
$state = 'tag name';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'before attribute name':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
// this conditional is optimized, check bottom
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Stay in the before attribute name state. */
|
||||
$state = 'before attribute name';
|
||||
|
||||
} elseif($char === '/') {
|
||||
/* U+002F SOLIDUS (/)
|
||||
Switch to the self-closing start tag state. */
|
||||
$state = 'self-closing start tag';
|
||||
|
||||
} elseif($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Emit the current tag token. Switch to the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
|
||||
} elseif('A' <= $char && $char <= 'Z') {
|
||||
/* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z
|
||||
Start a new attribute in the current tag token. Set that
|
||||
attribute's name to the lowercase version of the current
|
||||
input character (add 0x0020 to the character's code
|
||||
point), and its value to the empty string. Switch to the
|
||||
attribute name state.*/
|
||||
$this->token['attr'][] = array(
|
||||
'name' => strtolower($char),
|
||||
'value' => ''
|
||||
);
|
||||
|
||||
$state = 'attribute name';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Reconsume the EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-attribute-name-but-got-eof'
|
||||
));
|
||||
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* U+0022 QUOTATION MARK (")
|
||||
U+0027 APOSTROPHE (')
|
||||
U+003C LESS-THAN SIGN (<)
|
||||
U+003D EQUALS SIGN (=)
|
||||
Parse error. Treat it as per the "anything else" entry
|
||||
below. */
|
||||
if($char === '"' || $char === "'" || $char === '<' || $char === '=') {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'invalid-character-in-attribute-name'
|
||||
));
|
||||
}
|
||||
|
||||
/* Anything else
|
||||
Start a new attribute in the current tag token. Set that attribute's
|
||||
name to the current input character, and its value to the empty string.
|
||||
Switch to the attribute name state. */
|
||||
$this->token['attr'][] = array(
|
||||
'name' => $char,
|
||||
'value' => ''
|
||||
);
|
||||
|
||||
$state = 'attribute name';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'attribute name':
|
||||
// Consume the next input character:
|
||||
$char = $this->stream->char();
|
||||
|
||||
// this conditional is optimized, check bottom
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Switch to the after attribute name state. */
|
||||
$state = 'after attribute name';
|
||||
|
||||
} elseif($char === '/') {
|
||||
/* U+002F SOLIDUS (/)
|
||||
Switch to the self-closing start tag state. */
|
||||
$state = 'self-closing start tag';
|
||||
|
||||
} elseif($char === '=') {
|
||||
/* U+003D EQUALS SIGN (=)
|
||||
Switch to the before attribute value state. */
|
||||
$state = 'before attribute value';
|
||||
|
||||
} elseif($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Emit the current tag token. Switch to the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
|
||||
} elseif('A' <= $char && $char <= 'Z') {
|
||||
/* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z
|
||||
Append the lowercase version of the current input
|
||||
character (add 0x0020 to the character's code point) to
|
||||
the current attribute's name. Stay in the attribute name
|
||||
state. */
|
||||
$chars = $this->stream->charsWhile(self::UPPER_ALPHA);
|
||||
|
||||
$last = count($this->token['attr']) - 1;
|
||||
$this->token['attr'][$last]['name'] .= strtolower($char . $chars);
|
||||
|
||||
$state = 'attribute name';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Reconsume the EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-attribute-name'
|
||||
));
|
||||
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* U+0022 QUOTATION MARK (")
|
||||
U+0027 APOSTROPHE (')
|
||||
U+003C LESS-THAN SIGN (<)
|
||||
Parse error. Treat it as per the "anything else"
|
||||
entry below. */
|
||||
if($char === '"' || $char === "'" || $char === '<') {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'invalid-character-in-attribute-name'
|
||||
));
|
||||
}
|
||||
|
||||
/* Anything else
|
||||
Append the current input character to the current attribute's name.
|
||||
Stay in the attribute name state. */
|
||||
$chars = $this->stream->charsUntil("\t\n\x0C /=>\"'" . self::UPPER_ALPHA);
|
||||
|
||||
$last = count($this->token['attr']) - 1;
|
||||
$this->token['attr'][$last]['name'] .= $char . $chars;
|
||||
|
||||
$state = 'attribute name';
|
||||
}
|
||||
|
||||
/* When the user agent leaves the attribute name state
|
||||
(and before emitting the tag token, if appropriate), the
|
||||
complete attribute's name must be compared to the other
|
||||
attributes on the same token; if there is already an
|
||||
attribute on the token with the exact same name, then this
|
||||
is a parse error and the new attribute must be dropped, along
|
||||
with the value that gets associated with it (if any). */
|
||||
// this might be implemented in the emitToken method
|
||||
break;
|
||||
|
||||
case 'after attribute name':
|
||||
// Consume the next input character:
|
||||
$char = $this->stream->char();
|
||||
|
||||
// this is an optimized conditional, check the bottom
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Stay in the after attribute name state. */
|
||||
$state = 'after attribute name';
|
||||
|
||||
} elseif($char === '/') {
|
||||
/* U+002F SOLIDUS (/)
|
||||
Switch to the self-closing start tag state. */
|
||||
$state = 'self-closing start tag';
|
||||
|
||||
} elseif($char === '=') {
|
||||
/* U+003D EQUALS SIGN (=)
|
||||
Switch to the before attribute value state. */
|
||||
$state = 'before attribute value';
|
||||
|
||||
} elseif($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Emit the current tag token. Switch to the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
|
||||
} elseif('A' <= $char && $char <= 'Z') {
|
||||
/* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z
|
||||
Start a new attribute in the current tag token. Set that
|
||||
attribute's name to the lowercase version of the current
|
||||
input character (add 0x0020 to the character's code
|
||||
point), and its value to the empty string. Switch to the
|
||||
attribute name state. */
|
||||
$this->token['attr'][] = array(
|
||||
'name' => strtolower($char),
|
||||
'value' => ''
|
||||
);
|
||||
|
||||
$state = 'attribute name';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Reconsume the EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-end-of-tag-but-got-eof'
|
||||
));
|
||||
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* U+0022 QUOTATION MARK (")
|
||||
U+0027 APOSTROPHE (')
|
||||
U+003C LESS-THAN SIGN(<)
|
||||
Parse error. Treat it as per the "anything else"
|
||||
entry below. */
|
||||
if($char === '"' || $char === "'" || $char === "<") {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'invalid-character-after-attribute-name'
|
||||
));
|
||||
}
|
||||
|
||||
/* Anything else
|
||||
Start a new attribute in the current tag token. Set that attribute's
|
||||
name to the current input character, and its value to the empty string.
|
||||
Switch to the attribute name state. */
|
||||
$this->token['attr'][] = array(
|
||||
'name' => $char,
|
||||
'value' => ''
|
||||
);
|
||||
|
||||
$state = 'attribute name';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'before attribute value':
|
||||
// Consume the next input character:
|
||||
$char = $this->stream->char();
|
||||
|
||||
// this is an optimized conditional
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Stay in the before attribute value state. */
|
||||
$state = 'before attribute value';
|
||||
|
||||
} elseif($char === '"') {
|
||||
/* U+0022 QUOTATION MARK (")
|
||||
Switch to the attribute value (double-quoted) state. */
|
||||
$state = 'attribute value (double-quoted)';
|
||||
|
||||
} elseif($char === '&') {
|
||||
/* U+0026 AMPERSAND (&)
|
||||
Switch to the attribute value (unquoted) state and reconsume
|
||||
this input character. */
|
||||
$this->stream->unget();
|
||||
$state = 'attribute value (unquoted)';
|
||||
|
||||
} elseif($char === '\'') {
|
||||
/* U+0027 APOSTROPHE (')
|
||||
Switch to the attribute value (single-quoted) state. */
|
||||
$state = 'attribute value (single-quoted)';
|
||||
|
||||
} elseif($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Parse error. Emit the current tag token. Switch to the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-attribute-value-but-got-right-bracket'
|
||||
));
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Reconsume the EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-attribute-value-but-got-eof'
|
||||
));
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* U+003D EQUALS SIGN (=)
|
||||
* U+003C LESS-THAN SIGN (<)
|
||||
Parse error. Treat it as per the "anything else" entry below. */
|
||||
if($char === '=' || $char === '<') {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'equals-in-unquoted-attribute-value'
|
||||
));
|
||||
}
|
||||
|
||||
/* Anything else
|
||||
Append the current input character to the current attribute's value.
|
||||
Switch to the attribute value (unquoted) state. */
|
||||
$last = count($this->token['attr']) - 1;
|
||||
$this->token['attr'][$last]['value'] .= $char;
|
||||
|
||||
$state = 'attribute value (unquoted)';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'attribute value (double-quoted)':
|
||||
// Consume the next input character:
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === '"') {
|
||||
/* U+0022 QUOTATION MARK (")
|
||||
Switch to the after attribute value (quoted) state. */
|
||||
$state = 'after attribute value (quoted)';
|
||||
|
||||
} elseif($char === '&') {
|
||||
/* U+0026 AMPERSAND (&)
|
||||
Switch to the character reference in attribute value
|
||||
state, with the additional allowed character
|
||||
being U+0022 QUOTATION MARK ("). */
|
||||
$this->characterReferenceInAttributeValue('"');
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Reconsume the EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-attribute-value-double-quote'
|
||||
));
|
||||
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Append the current input character to the current attribute's value.
|
||||
Stay in the attribute value (double-quoted) state. */
|
||||
$chars = $this->stream->charsUntil('"&');
|
||||
|
||||
$last = count($this->token['attr']) - 1;
|
||||
$this->token['attr'][$last]['value'] .= $char . $chars;
|
||||
|
||||
$state = 'attribute value (double-quoted)';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'attribute value (single-quoted)':
|
||||
// Consume the next input character:
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === "'") {
|
||||
/* U+0022 QUOTATION MARK (')
|
||||
Switch to the after attribute value state. */
|
||||
$state = 'after attribute value (quoted)';
|
||||
|
||||
} elseif($char === '&') {
|
||||
/* U+0026 AMPERSAND (&)
|
||||
Switch to the entity in attribute value state. */
|
||||
$this->characterReferenceInAttributeValue("'");
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Reconsume the EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-attribute-value-single-quote'
|
||||
));
|
||||
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Append the current input character to the current attribute's value.
|
||||
Stay in the attribute value (single-quoted) state. */
|
||||
$chars = $this->stream->charsUntil("'&");
|
||||
|
||||
$last = count($this->token['attr']) - 1;
|
||||
$this->token['attr'][$last]['value'] .= $char . $chars;
|
||||
|
||||
$state = 'attribute value (single-quoted)';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'attribute value (unquoted)':
|
||||
// Consume the next input character:
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Switch to the before attribute name state. */
|
||||
$state = 'before attribute name';
|
||||
|
||||
} elseif($char === '&') {
|
||||
/* U+0026 AMPERSAND (&)
|
||||
Switch to the entity in attribute value state, with the
|
||||
additional allowed character being U+003E
|
||||
GREATER-THAN SIGN (>). */
|
||||
$this->characterReferenceInAttributeValue('>');
|
||||
|
||||
} elseif($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Emit the current tag token. Switch to the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
|
||||
} elseif ($char === false) {
|
||||
/* EOF
|
||||
Parse error. Reconsume the EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-attribute-value-no-quotes'
|
||||
));
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* U+0022 QUOTATION MARK (")
|
||||
U+0027 APOSTROPHE (')
|
||||
U+003C LESS-THAN SIGN (<)
|
||||
U+003D EQUALS SIGN (=)
|
||||
Parse error. Treat it as per the "anything else"
|
||||
entry below. */
|
||||
if($char === '"' || $char === "'" || $char === '=' || $char == '<') {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-character-in-unquoted-attribute-value'
|
||||
));
|
||||
}
|
||||
|
||||
/* Anything else
|
||||
Append the current input character to the current attribute's value.
|
||||
Stay in the attribute value (unquoted) state. */
|
||||
$chars = $this->stream->charsUntil("\t\n\x0c &>\"'=");
|
||||
|
||||
$last = count($this->token['attr']) - 1;
|
||||
$this->token['attr'][$last]['value'] .= $char . $chars;
|
||||
|
||||
$state = 'attribute value (unquoted)';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'after attribute value (quoted)':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Switch to the before attribute name state. */
|
||||
$state = 'before attribute name';
|
||||
|
||||
} elseif ($char === '/') {
|
||||
/* U+002F SOLIDUS (/)
|
||||
Switch to the self-closing start tag state. */
|
||||
$state = 'self-closing start tag';
|
||||
|
||||
} elseif ($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Emit the current tag token. Switch to the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
|
||||
} elseif ($char === false) {
|
||||
/* EOF
|
||||
Parse error. Reconsume the EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-EOF-after-attribute-value'
|
||||
));
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Parse error. Reconsume the character in the before attribute
|
||||
name state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-character-after-attribute-value'
|
||||
));
|
||||
$this->stream->unget();
|
||||
$state = 'before attribute name';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'self-closing start tag':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if ($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Set the self-closing flag of the current tag token.
|
||||
Emit the current tag token. Switch to the data state. */
|
||||
// not sure if this is the name we want
|
||||
$this->token['self-closing'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
|
||||
} elseif ($char === false) {
|
||||
/* EOF
|
||||
Parse error. Reconsume the EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-eof-after-self-closing'
|
||||
));
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Parse error. Reconsume the character in the before attribute name state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-character-after-self-closing'
|
||||
));
|
||||
$this->stream->unget();
|
||||
$state = 'before attribute name';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bogus comment':
|
||||
/* (This can only happen if the content model flag is set to the PCDATA state.) */
|
||||
/* Consume every character up to the first U+003E GREATER-THAN SIGN
|
||||
character (>) or the end of the file (EOF), whichever comes first. Emit
|
||||
a comment token whose data is the concatenation of all the characters
|
||||
starting from and including the character that caused the state machine
|
||||
to switch into the bogus comment state, up to and including the last
|
||||
consumed character before the U+003E character, if any, or up to the
|
||||
end of the file otherwise. (If the comment was started by the end of
|
||||
the file (EOF), the token is empty.) */
|
||||
$this->token['data'] .= (string) $this->stream->charsUntil('>');
|
||||
$this->stream->char();
|
||||
|
||||
$this->emitToken($this->token);
|
||||
|
||||
/* Switch to the data state. */
|
||||
$state = 'data';
|
||||
break;
|
||||
|
||||
case 'markup declaration open':
|
||||
// Consume for below
|
||||
$hyphens = $this->stream->charsWhile('-', 2);
|
||||
if ($hyphens === '-') {
|
||||
$this->stream->unget();
|
||||
}
|
||||
if ($hyphens !== '--') {
|
||||
$alpha = $this->stream->charsWhile(self::ALPHA, 7);
|
||||
}
|
||||
|
||||
/* If the next two characters are both U+002D HYPHEN-MINUS (-)
|
||||
characters, consume those two characters, create a comment token whose
|
||||
data is the empty string, and switch to the comment state. */
|
||||
if($hyphens === '--') {
|
||||
$state = 'comment start';
|
||||
$this->token = array(
|
||||
'data' => '',
|
||||
'type' => self::COMMENT
|
||||
);
|
||||
|
||||
/* Otherwise if the next seven characters are a case-insensitive match
|
||||
for the word "DOCTYPE", then consume those characters and switch to the
|
||||
DOCTYPE state. */
|
||||
} elseif(strtoupper($alpha) === 'DOCTYPE') {
|
||||
$state = 'DOCTYPE';
|
||||
|
||||
// XXX not implemented
|
||||
/* Otherwise, if the insertion mode is "in foreign content"
|
||||
and the current node is not an element in the HTML namespace
|
||||
and the next seven characters are an ASCII case-sensitive
|
||||
match for the string "[CDATA[" (the five uppercase letters
|
||||
"CDATA" with a U+005B LEFT SQUARE BRACKET character before
|
||||
and after), then consume those characters and switch to the
|
||||
CDATA section state (which is unrelated to the content model
|
||||
flag's CDATA state). */
|
||||
|
||||
/* Otherwise, is is a parse error. Switch to the bogus comment state.
|
||||
The next character that is consumed, if any, is the first character
|
||||
that will be in the comment. */
|
||||
} else {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-dashes-or-doctype'
|
||||
));
|
||||
$this->token = array(
|
||||
'data' => (string) $alpha,
|
||||
'type' => self::COMMENT
|
||||
);
|
||||
$state = 'bogus comment';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'comment start':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if ($char === '-') {
|
||||
/* U+002D HYPHEN-MINUS (-)
|
||||
Switch to the comment start dash state. */
|
||||
$state = 'comment start dash';
|
||||
} elseif ($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Parse error. Emit the comment token. Switch to the
|
||||
data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'incorrect-comment'
|
||||
));
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
} elseif ($char === false) {
|
||||
/* EOF
|
||||
Parse error. Emit the comment token. Reconsume the
|
||||
EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-comment'
|
||||
));
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
} else {
|
||||
/* Anything else
|
||||
Append the input character to the comment token's
|
||||
data. Switch to the comment state. */
|
||||
$this->token['data'] .= $char;
|
||||
$state = 'comment';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'comment start dash':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
if ($char === '-') {
|
||||
/* U+002D HYPHEN-MINUS (-)
|
||||
Switch to the comment end state */
|
||||
$state = 'comment end';
|
||||
} elseif ($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Parse error. Emit the comment token. Switch to the
|
||||
data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'incorrect-comment'
|
||||
));
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
} elseif ($char === false) {
|
||||
/* Parse error. Emit the comment token. Reconsume the
|
||||
EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-comment'
|
||||
));
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
} else {
|
||||
$this->token['data'] .= '-' . $char;
|
||||
$state = 'comment';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'comment':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === '-') {
|
||||
/* U+002D HYPHEN-MINUS (-)
|
||||
Switch to the comment end dash state */
|
||||
$state = 'comment end dash';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Emit the comment token. Reconsume the EOF character
|
||||
in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-comment'
|
||||
));
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Append the input character to the comment token's data. Stay in
|
||||
the comment state. */
|
||||
$chars = $this->stream->charsUntil('-');
|
||||
|
||||
$this->token['data'] .= $char . $chars;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'comment end dash':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === '-') {
|
||||
/* U+002D HYPHEN-MINUS (-)
|
||||
Switch to the comment end state */
|
||||
$state = 'comment end';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Emit the comment token. Reconsume the EOF character
|
||||
in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-comment-end-dash'
|
||||
));
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Append a U+002D HYPHEN-MINUS (-) character and the input
|
||||
character to the comment token's data. Switch to the comment state. */
|
||||
$this->token['data'] .= '-'.$char;
|
||||
$state = 'comment';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'comment end':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Emit the comment token. Switch to the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
|
||||
} elseif($char === '-') {
|
||||
/* U+002D HYPHEN-MINUS (-)
|
||||
Parse error. Append a U+002D HYPHEN-MINUS (-) character
|
||||
to the comment token's data. Stay in the comment end
|
||||
state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-dash-after-double-dash-in-comment'
|
||||
));
|
||||
$this->token['data'] .= '-';
|
||||
|
||||
} elseif($char === "\t" || $char === "\n" || $char === "\x0a" || $char === ' ') {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-space-after-double-dash-in-comment'
|
||||
));
|
||||
$this->token['data'] .= '--' . $char;
|
||||
$state = 'comment end space';
|
||||
|
||||
} elseif($char === '!') {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-bang-after-double-dash-in-comment'
|
||||
));
|
||||
$state = 'comment end bang';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Emit the comment token. Reconsume the
|
||||
EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-comment-double-dash'
|
||||
));
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Parse error. Append two U+002D HYPHEN-MINUS (-)
|
||||
characters and the input character to the comment token's
|
||||
data. Switch to the comment state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-char-in-comment'
|
||||
));
|
||||
$this->token['data'] .= '--'.$char;
|
||||
$state = 'comment';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'comment end bang':
|
||||
$char = $this->stream->char();
|
||||
if ($char === '>') {
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
} elseif ($char === "-") {
|
||||
$this->token['data'] .= '--!';
|
||||
$state = 'comment end dash';
|
||||
} elseif ($char === false) {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-comment-end-bang'
|
||||
));
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
} else {
|
||||
$this->token['data'] .= '--!' . $char;
|
||||
$state = 'comment';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'comment end space':
|
||||
$char = $this->stream->char();
|
||||
if ($char === '>') {
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
} elseif ($char === '-') {
|
||||
$state = 'comment end dash';
|
||||
} elseif ($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
$this->token['data'] .= $char;
|
||||
} elseif ($char === false) {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-eof-in-comment-end-space',
|
||||
));
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
} else {
|
||||
$this->token['data'] .= $char;
|
||||
$state = 'comment';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DOCTYPE':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Switch to the before DOCTYPE name state. */
|
||||
$state = 'before DOCTYPE name';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Create a new DOCTYPE token. Set its
|
||||
force-quirks flag to on. Emit the token. Reconsume the
|
||||
EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'need-space-after-doctype-but-got-eof'
|
||||
));
|
||||
$this->emitToken(array(
|
||||
'name' => '',
|
||||
'type' => self::DOCTYPE,
|
||||
'force-quirks' => true,
|
||||
'error' => true
|
||||
));
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Parse error. Reconsume the current character in the
|
||||
before DOCTYPE name state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'need-space-after-doctype'
|
||||
));
|
||||
$this->stream->unget();
|
||||
$state = 'before DOCTYPE name';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'before DOCTYPE name':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Stay in the before DOCTYPE name state. */
|
||||
|
||||
} elseif($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Parse error. Create a new DOCTYPE token. Set its
|
||||
force-quirks flag to on. Emit the token. Switch to the
|
||||
data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-doctype-name-but-got-right-bracket'
|
||||
));
|
||||
$this->emitToken(array(
|
||||
'name' => '',
|
||||
'type' => self::DOCTYPE,
|
||||
'force-quirks' => true,
|
||||
'error' => true
|
||||
));
|
||||
|
||||
$state = 'data';
|
||||
|
||||
} elseif('A' <= $char && $char <= 'Z') {
|
||||
/* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z
|
||||
Create a new DOCTYPE token. Set the token's name to the
|
||||
lowercase version of the input character (add 0x0020 to
|
||||
the character's code point). Switch to the DOCTYPE name
|
||||
state. */
|
||||
$this->token = array(
|
||||
'name' => strtolower($char),
|
||||
'type' => self::DOCTYPE,
|
||||
'error' => true
|
||||
);
|
||||
|
||||
$state = 'DOCTYPE name';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Create a new DOCTYPE token. Set its
|
||||
force-quirks flag to on. Emit the token. Reconsume the
|
||||
EOF character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-doctype-name-but-got-eof'
|
||||
));
|
||||
$this->emitToken(array(
|
||||
'name' => '',
|
||||
'type' => self::DOCTYPE,
|
||||
'force-quirks' => true,
|
||||
'error' => true
|
||||
));
|
||||
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Create a new DOCTYPE token. Set the token's name to the
|
||||
current input character. Switch to the DOCTYPE name state. */
|
||||
$this->token = array(
|
||||
'name' => $char,
|
||||
'type' => self::DOCTYPE,
|
||||
'error' => true
|
||||
);
|
||||
|
||||
$state = 'DOCTYPE name';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DOCTYPE name':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Switch to the after DOCTYPE name state. */
|
||||
$state = 'after DOCTYPE name';
|
||||
|
||||
} elseif($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Emit the current DOCTYPE token. Switch to the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
|
||||
} elseif('A' <= $char && $char <= 'Z') {
|
||||
/* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z
|
||||
Append the lowercase version of the input character
|
||||
(add 0x0020 to the character's code point) to the current
|
||||
DOCTYPE token's name. Stay in the DOCTYPE name state. */
|
||||
$this->token['name'] .= strtolower($char);
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Emit that DOCTYPE token. Reconsume the EOF
|
||||
character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-doctype-name'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Append the current input character to the current
|
||||
DOCTYPE token's name. Stay in the DOCTYPE name state. */
|
||||
$this->token['name'] .= $char;
|
||||
}
|
||||
|
||||
// XXX this is probably some sort of quirks mode designation,
|
||||
// check tree-builder to be sure. In general 'error' needs
|
||||
// to be specc'ified, this probably means removing it at the end
|
||||
$this->token['error'] = ($this->token['name'] === 'HTML')
|
||||
? false
|
||||
: true;
|
||||
break;
|
||||
|
||||
case 'after DOCTYPE name':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Stay in the after DOCTYPE name state. */
|
||||
|
||||
} elseif($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Emit the current DOCTYPE token. Switch to the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Emit that DOCTYPE token. Reconsume the EOF
|
||||
character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else */
|
||||
|
||||
$nextSix = strtoupper($char . $this->stream->charsWhile(self::ALPHA, 5));
|
||||
if ($nextSix === 'PUBLIC') {
|
||||
/* If the next six characters are an ASCII
|
||||
case-insensitive match for the word "PUBLIC", then
|
||||
consume those characters and switch to the before
|
||||
DOCTYPE public identifier state. */
|
||||
$state = 'before DOCTYPE public identifier';
|
||||
|
||||
} elseif ($nextSix === 'SYSTEM') {
|
||||
/* Otherwise, if the next six characters are an ASCII
|
||||
case-insensitive match for the word "SYSTEM", then
|
||||
consume those characters and switch to the before
|
||||
DOCTYPE system identifier state. */
|
||||
$state = 'before DOCTYPE system identifier';
|
||||
|
||||
} else {
|
||||
/* Otherwise, this is the parse error. Set the DOCTYPE
|
||||
token's force-quirks flag to on. Switch to the bogus
|
||||
DOCTYPE state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-space-or-right-bracket-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->token['error'] = true;
|
||||
$state = 'bogus DOCTYPE';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'before DOCTYPE public identifier':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Stay in the before DOCTYPE public identifier state. */
|
||||
} elseif ($char === '"') {
|
||||
/* U+0022 QUOTATION MARK (")
|
||||
Set the DOCTYPE token's public identifier to the empty
|
||||
string (not missing), then switch to the DOCTYPE public
|
||||
identifier (double-quoted) state. */
|
||||
$this->token['public'] = '';
|
||||
$state = 'DOCTYPE public identifier (double-quoted)';
|
||||
} elseif ($char === "'") {
|
||||
/* U+0027 APOSTROPHE (')
|
||||
Set the DOCTYPE token's public identifier to the empty
|
||||
string (not missing), then switch to the DOCTYPE public
|
||||
identifier (single-quoted) state. */
|
||||
$this->token['public'] = '';
|
||||
$state = 'DOCTYPE public identifier (single-quoted)';
|
||||
} elseif ($char === '>') {
|
||||
/* Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Emit that DOCTYPE token. Switch to the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-end-of-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
} elseif ($char === false) {
|
||||
/* Parse error. Set the DOCTYPE token's force-quirks
|
||||
flag to on. Emit that DOCTYPE token. Reconsume the EOF
|
||||
character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
} else {
|
||||
/* Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Switch to the bogus DOCTYPE state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-char-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$state = 'bogus DOCTYPE';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DOCTYPE public identifier (double-quoted)':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if ($char === '"') {
|
||||
/* U+0022 QUOTATION MARK (")
|
||||
Switch to the after DOCTYPE public identifier state. */
|
||||
$state = 'after DOCTYPE public identifier';
|
||||
} elseif ($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Emit that DOCTYPE token. Switch to the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-end-of-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
} elseif ($char === false) {
|
||||
/* EOF
|
||||
Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Emit that DOCTYPE token. Reconsume the EOF
|
||||
character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
} else {
|
||||
/* Anything else
|
||||
Append the current input character to the current
|
||||
DOCTYPE token's public identifier. Stay in the DOCTYPE
|
||||
public identifier (double-quoted) state. */
|
||||
$this->token['public'] .= $char;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DOCTYPE public identifier (single-quoted)':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if ($char === "'") {
|
||||
/* U+0027 APOSTROPHE (')
|
||||
Switch to the after DOCTYPE public identifier state. */
|
||||
$state = 'after DOCTYPE public identifier';
|
||||
} elseif ($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Emit that DOCTYPE token. Switch to the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-end-of-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
} elseif ($char === false) {
|
||||
/* EOF
|
||||
Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Emit that DOCTYPE token. Reconsume the EOF
|
||||
character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
} else {
|
||||
/* Anything else
|
||||
Append the current input character to the current
|
||||
DOCTYPE token's public identifier. Stay in the DOCTYPE
|
||||
public identifier (double-quoted) state. */
|
||||
$this->token['public'] .= $char;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'after DOCTYPE public identifier':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Stay in the after DOCTYPE public identifier state. */
|
||||
} elseif ($char === '"') {
|
||||
/* U+0022 QUOTATION MARK (")
|
||||
Set the DOCTYPE token's system identifier to the
|
||||
empty string (not missing), then switch to the DOCTYPE
|
||||
system identifier (double-quoted) state. */
|
||||
$this->token['system'] = '';
|
||||
$state = 'DOCTYPE system identifier (double-quoted)';
|
||||
} elseif ($char === "'") {
|
||||
/* U+0027 APOSTROPHE (')
|
||||
Set the DOCTYPE token's system identifier to the
|
||||
empty string (not missing), then switch to the DOCTYPE
|
||||
system identifier (single-quoted) state. */
|
||||
$this->token['system'] = '';
|
||||
$state = 'DOCTYPE system identifier (single-quoted)';
|
||||
} elseif ($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Emit the current DOCTYPE token. Switch to the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
} elseif ($char === false) {
|
||||
/* Parse error. Set the DOCTYPE token's force-quirks
|
||||
flag to on. Emit that DOCTYPE token. Reconsume the EOF
|
||||
character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
} else {
|
||||
/* Anything else
|
||||
Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Switch to the bogus DOCTYPE state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-char-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$state = 'bogus DOCTYPE';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'before DOCTYPE system identifier':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Stay in the before DOCTYPE system identifier state. */
|
||||
} elseif ($char === '"') {
|
||||
/* U+0022 QUOTATION MARK (")
|
||||
Set the DOCTYPE token's system identifier to the empty
|
||||
string (not missing), then switch to the DOCTYPE system
|
||||
identifier (double-quoted) state. */
|
||||
$this->token['system'] = '';
|
||||
$state = 'DOCTYPE system identifier (double-quoted)';
|
||||
} elseif ($char === "'") {
|
||||
/* U+0027 APOSTROPHE (')
|
||||
Set the DOCTYPE token's system identifier to the empty
|
||||
string (not missing), then switch to the DOCTYPE system
|
||||
identifier (single-quoted) state. */
|
||||
$this->token['system'] = '';
|
||||
$state = 'DOCTYPE system identifier (single-quoted)';
|
||||
} elseif ($char === '>') {
|
||||
/* Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Emit that DOCTYPE token. Switch to the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-char-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
} elseif ($char === false) {
|
||||
/* Parse error. Set the DOCTYPE token's force-quirks
|
||||
flag to on. Emit that DOCTYPE token. Reconsume the EOF
|
||||
character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
} else {
|
||||
/* Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Switch to the bogus DOCTYPE state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-char-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$state = 'bogus DOCTYPE';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DOCTYPE system identifier (double-quoted)':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if ($char === '"') {
|
||||
/* U+0022 QUOTATION MARK (")
|
||||
Switch to the after DOCTYPE system identifier state. */
|
||||
$state = 'after DOCTYPE system identifier';
|
||||
} elseif ($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Emit that DOCTYPE token. Switch to the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-end-of-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
} elseif ($char === false) {
|
||||
/* EOF
|
||||
Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Emit that DOCTYPE token. Reconsume the EOF
|
||||
character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
} else {
|
||||
/* Anything else
|
||||
Append the current input character to the current
|
||||
DOCTYPE token's system identifier. Stay in the DOCTYPE
|
||||
system identifier (double-quoted) state. */
|
||||
$this->token['system'] .= $char;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DOCTYPE system identifier (single-quoted)':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if ($char === "'") {
|
||||
/* U+0027 APOSTROPHE (')
|
||||
Switch to the after DOCTYPE system identifier state. */
|
||||
$state = 'after DOCTYPE system identifier';
|
||||
} elseif ($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Emit that DOCTYPE token. Switch to the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-end-of-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
} elseif ($char === false) {
|
||||
/* EOF
|
||||
Parse error. Set the DOCTYPE token's force-quirks flag
|
||||
to on. Emit that DOCTYPE token. Reconsume the EOF
|
||||
character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
} else {
|
||||
/* Anything else
|
||||
Append the current input character to the current
|
||||
DOCTYPE token's system identifier. Stay in the DOCTYPE
|
||||
system identifier (double-quoted) state. */
|
||||
$this->token['system'] .= $char;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'after DOCTYPE system identifier':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
Stay in the after DOCTYPE system identifier state. */
|
||||
} elseif ($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Emit the current DOCTYPE token. Switch to the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
} elseif ($char === false) {
|
||||
/* Parse error. Set the DOCTYPE token's force-quirks
|
||||
flag to on. Emit that DOCTYPE token. Reconsume the EOF
|
||||
character in the data state. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'eof-in-doctype'
|
||||
));
|
||||
$this->token['force-quirks'] = true;
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
} else {
|
||||
/* Anything else
|
||||
Parse error. Switch to the bogus DOCTYPE state.
|
||||
(This does not set the DOCTYPE token's force-quirks
|
||||
flag to on.) */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'unexpected-char-in-doctype'
|
||||
));
|
||||
$state = 'bogus DOCTYPE';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bogus DOCTYPE':
|
||||
/* Consume the next input character: */
|
||||
$char = $this->stream->char();
|
||||
|
||||
if ($char === '>') {
|
||||
/* U+003E GREATER-THAN SIGN (>)
|
||||
Emit the DOCTYPE token. Switch to the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$state = 'data';
|
||||
|
||||
} elseif($char === false) {
|
||||
/* EOF
|
||||
Emit the DOCTYPE token. Reconsume the EOF character in
|
||||
the data state. */
|
||||
$this->emitToken($this->token);
|
||||
$this->stream->unget();
|
||||
$state = 'data';
|
||||
|
||||
} else {
|
||||
/* Anything else
|
||||
Stay in the bogus DOCTYPE state. */
|
||||
}
|
||||
break;
|
||||
|
||||
// case 'cdataSection':
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized representation of the tree.
|
||||
*/
|
||||
public function save() {
|
||||
return $this->tree->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the input stream.
|
||||
*/
|
||||
public function stream() {
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
private function consumeCharacterReference($allowed = false, $inattr = false) {
|
||||
// This goes quite far against spec, and is far closer to the Python
|
||||
// impl., mainly because we don't do the large unconsuming the spec
|
||||
// requires.
|
||||
|
||||
// All consumed characters.
|
||||
$chars = $this->stream->char();
|
||||
|
||||
/* This section defines how to consume a character
|
||||
reference. This definition is used when parsing character
|
||||
references in text and in attributes.
|
||||
|
||||
The behavior depends on the identity of the next character
|
||||
(the one immediately after the U+0026 AMPERSAND character): */
|
||||
|
||||
if (
|
||||
$chars[0] === "\x09" ||
|
||||
$chars[0] === "\x0A" ||
|
||||
$chars[0] === "\x0C" ||
|
||||
$chars[0] === "\x20" ||
|
||||
$chars[0] === '<' ||
|
||||
$chars[0] === '&' ||
|
||||
$chars === false ||
|
||||
$chars[0] === $allowed
|
||||
) {
|
||||
/* U+0009 CHARACTER TABULATION
|
||||
U+000A LINE FEED (LF)
|
||||
U+000C FORM FEED (FF)
|
||||
U+0020 SPACE
|
||||
U+003C LESS-THAN SIGN
|
||||
U+0026 AMPERSAND
|
||||
EOF
|
||||
The additional allowed character, if there is one
|
||||
Not a character reference. No characters are consumed,
|
||||
and nothing is returned. (This is not an error, either.) */
|
||||
// We already consumed, so unconsume.
|
||||
$this->stream->unget();
|
||||
return '&';
|
||||
} elseif ($chars[0] === '#') {
|
||||
/* Consume the U+0023 NUMBER SIGN. */
|
||||
// Um, yeah, we already did that.
|
||||
/* The behavior further depends on the character after
|
||||
the U+0023 NUMBER SIGN: */
|
||||
$chars .= $this->stream->char();
|
||||
if (isset($chars[1]) && ($chars[1] === 'x' || $chars[1] === 'X')) {
|
||||
/* U+0078 LATIN SMALL LETTER X
|
||||
U+0058 LATIN CAPITAL LETTER X */
|
||||
/* Consume the X. */
|
||||
// Um, yeah, we already did that.
|
||||
/* Follow the steps below, but using the range of
|
||||
characters U+0030 DIGIT ZERO through to U+0039 DIGIT
|
||||
NINE, U+0061 LATIN SMALL LETTER A through to U+0066
|
||||
LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER
|
||||
A, through to U+0046 LATIN CAPITAL LETTER F (in other
|
||||
words, 0123456789, ABCDEF, abcdef). */
|
||||
$char_class = self::HEX;
|
||||
/* When it comes to interpreting the
|
||||
number, interpret it as a hexadecimal number. */
|
||||
$hex = true;
|
||||
} else {
|
||||
/* Anything else */
|
||||
// Unconsume because we shouldn't have consumed this.
|
||||
$chars = $chars[0];
|
||||
$this->stream->unget();
|
||||
/* Follow the steps below, but using the range of
|
||||
characters U+0030 DIGIT ZERO through to U+0039 DIGIT
|
||||
NINE (i.e. just 0123456789). */
|
||||
$char_class = self::DIGIT;
|
||||
/* When it comes to interpreting the number,
|
||||
interpret it as a decimal number. */
|
||||
$hex = false;
|
||||
}
|
||||
|
||||
/* Consume as many characters as match the range of characters given above. */
|
||||
$consumed = $this->stream->charsWhile($char_class);
|
||||
if ($consumed === '' || $consumed === false) {
|
||||
/* If no characters match the range, then don't consume
|
||||
any characters (and unconsume the U+0023 NUMBER SIGN
|
||||
character and, if appropriate, the X character). This
|
||||
is a parse error; nothing is returned. */
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-numeric-entity'
|
||||
));
|
||||
return '&' . $chars;
|
||||
} else {
|
||||
/* Otherwise, if the next character is a U+003B SEMICOLON,
|
||||
consume that too. If it isn't, there is a parse error. */
|
||||
if ($this->stream->char() !== ';') {
|
||||
$this->stream->unget();
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'numeric-entity-without-semicolon'
|
||||
));
|
||||
}
|
||||
|
||||
/* If one or more characters match the range, then take
|
||||
them all and interpret the string of characters as a number
|
||||
(either hexadecimal or decimal as appropriate). */
|
||||
$codepoint = $hex ? hexdec($consumed) : (int) $consumed;
|
||||
|
||||
/* If that number is one of the numbers in the first column
|
||||
of the following table, then this is a parse error. Find the
|
||||
row with that number in the first column, and return a
|
||||
character token for the Unicode character given in the
|
||||
second column of that row. */
|
||||
$new_codepoint = HTML5_Data::getRealCodepoint($codepoint);
|
||||
if ($new_codepoint) {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'illegal-windows-1252-entity'
|
||||
));
|
||||
return HTML5_Data::utf8chr($new_codepoint);
|
||||
} else {
|
||||
/* Otherwise, if the number is greater than 0x10FFFF, then
|
||||
* this is a parse error. Return a U+FFFD REPLACEMENT
|
||||
* CHARACTER. */
|
||||
if ($codepoint > 0x10FFFF) {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'overlong-character-entity' // XXX probably not correct
|
||||
));
|
||||
return "\xEF\xBF\xBD";
|
||||
}
|
||||
/* Otherwise, return a character token for the Unicode
|
||||
* character whose code point is that number. If the
|
||||
* number is in the range 0x0001 to 0x0008, 0x000E to
|
||||
* 0x001F, 0x007F to 0x009F, 0xD800 to 0xDFFF, 0xFDD0 to
|
||||
* 0xFDEF, or is one of 0x000B, 0xFFFE, 0xFFFF, 0x1FFFE,
|
||||
* 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF, 0x4FFFE,
|
||||
* 0x4FFFF, 0x5FFFE, 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE,
|
||||
* 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE, 0x9FFFF, 0xAFFFE,
|
||||
* 0xAFFFF, 0xBFFFE, 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE,
|
||||
* 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE,
|
||||
* or 0x10FFFF, then this is a parse error. */
|
||||
// && has higher precedence than ||
|
||||
if (
|
||||
$codepoint >= 0x0000 && $codepoint <= 0x0008 ||
|
||||
$codepoint === 0x000B ||
|
||||
$codepoint >= 0x000E && $codepoint <= 0x001F ||
|
||||
$codepoint >= 0x007F && $codepoint <= 0x009F ||
|
||||
$codepoint >= 0xD800 && $codepoint <= 0xDFFF ||
|
||||
$codepoint >= 0xFDD0 && $codepoint <= 0xFDEF ||
|
||||
($codepoint & 0xFFFE) === 0xFFFE ||
|
||||
$codepoint == 0x10FFFF || $codepoint == 0x10FFFE
|
||||
) {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'illegal-codepoint-for-numeric-entity'
|
||||
));
|
||||
}
|
||||
return HTML5_Data::utf8chr($codepoint);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
/* Anything else */
|
||||
|
||||
/* Consume the maximum number of characters possible,
|
||||
with the consumed characters matching one of the
|
||||
identifiers in the first column of the named character
|
||||
references table (in a case-sensitive manner). */
|
||||
// What we actually do here is consume as much as we can while it
|
||||
// matches the start of one of the identifiers in the first column.
|
||||
|
||||
$refs = HTML5_Data::getNamedCharacterReferences();
|
||||
|
||||
// Get the longest string which is the start of an identifier
|
||||
// ($chars) as well as the longest identifier which matches ($id)
|
||||
// and its codepoint ($codepoint).
|
||||
$codepoint = false;
|
||||
$char = $chars;
|
||||
while ($char !== false && isset($refs[$char])) {
|
||||
$refs = $refs[$char];
|
||||
if (isset($refs['codepoint'])) {
|
||||
$id = $chars;
|
||||
$codepoint = $refs['codepoint'];
|
||||
}
|
||||
$chars .= $char = $this->stream->char();
|
||||
}
|
||||
|
||||
// Unconsume the one character we just took which caused the while
|
||||
// statement to fail. This could be anything and could cause state
|
||||
// changes (as if it matches the while loop it must be
|
||||
// alphanumeric so we can just concat it to whatever we get later).
|
||||
$this->stream->unget();
|
||||
if ($char !== false) {
|
||||
$chars = substr($chars, 0, -1);
|
||||
}
|
||||
|
||||
/* If no match can be made, then this is a parse error.
|
||||
No characters are consumed, and nothing is returned. */
|
||||
if (!$codepoint) {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'expected-named-entity'
|
||||
));
|
||||
return '&' . $chars;
|
||||
}
|
||||
|
||||
/* If the last character matched is not a U+003B SEMICOLON
|
||||
(;), there is a parse error. */
|
||||
$semicolon = true;
|
||||
if (substr($id, -1) !== ';') {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'named-entity-without-semicolon'
|
||||
));
|
||||
$semicolon = false;
|
||||
}
|
||||
|
||||
/* If the character reference is being consumed as part of
|
||||
an attribute, and the last character matched is not a
|
||||
U+003B SEMICOLON (;), and the next character is in the
|
||||
range U+0030 DIGIT ZERO to U+0039 DIGIT NINE, U+0041
|
||||
LATIN CAPITAL LETTER A to U+005A LATIN CAPITAL LETTER Z,
|
||||
or U+0061 LATIN SMALL LETTER A to U+007A LATIN SMALL LETTER Z,
|
||||
then, for historical reasons, all the characters that were
|
||||
matched after the U+0026 AMPERSAND (&) must be unconsumed,
|
||||
and nothing is returned. */
|
||||
if ($inattr && !$semicolon) {
|
||||
// The next character is either the next character in $chars or in the stream.
|
||||
if (strlen($chars) > strlen($id)) {
|
||||
$next = substr($chars, strlen($id), 1);
|
||||
} else {
|
||||
$next = $this->stream->char();
|
||||
$this->stream->unget();
|
||||
}
|
||||
if (
|
||||
'0' <= $next && $next <= '9' ||
|
||||
'A' <= $next && $next <= 'Z' ||
|
||||
'a' <= $next && $next <= 'z'
|
||||
) {
|
||||
return '&' . $chars;
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise, return a character token for the character
|
||||
corresponding to the character reference name (as given
|
||||
by the second column of the named character references table). */
|
||||
return HTML5_Data::utf8chr($codepoint) . substr($chars, strlen($id));
|
||||
}
|
||||
}
|
||||
|
||||
private function characterReferenceInAttributeValue($allowed = false) {
|
||||
/* Attempt to consume a character reference. */
|
||||
$entity = $this->consumeCharacterReference($allowed, true);
|
||||
|
||||
/* If nothing is returned, append a U+0026 AMPERSAND
|
||||
character to the current attribute's value.
|
||||
|
||||
Otherwise, append the returned character token to the
|
||||
current attribute's value. */
|
||||
$char = (!$entity)
|
||||
? '&'
|
||||
: $entity;
|
||||
|
||||
$last = count($this->token['attr']) - 1;
|
||||
$this->token['attr'][$last]['value'] .= $char;
|
||||
|
||||
/* Finally, switch back to the attribute value state that you
|
||||
were in when were switched into this state. */
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a token, passing it on to the tree builder.
|
||||
*/
|
||||
protected function emitToken($token, $checkStream = true, $dry = false) {
|
||||
if ($checkStream) {
|
||||
// Emit errors from input stream.
|
||||
while ($this->stream->errors) {
|
||||
$this->emitToken(array_shift($this->stream->errors), false);
|
||||
}
|
||||
}
|
||||
if($token['type'] === self::ENDTAG && !empty($token['attr'])) {
|
||||
for ($i = 0; $i < count($token['attr']); $i++) {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'attributes-in-end-tag'
|
||||
));
|
||||
}
|
||||
}
|
||||
if($token['type'] === self::ENDTAG && !empty($token['self-closing'])) {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'self-closing-flag-on-end-tag',
|
||||
));
|
||||
}
|
||||
if($token['type'] === self::STARTTAG) {
|
||||
// This could be changed to actually pass the tree-builder a hash
|
||||
$hash = array();
|
||||
foreach ($token['attr'] as $keypair) {
|
||||
if (isset($hash[$keypair['name']])) {
|
||||
$this->emitToken(array(
|
||||
'type' => self::PARSEERROR,
|
||||
'data' => 'duplicate-attribute',
|
||||
));
|
||||
} else {
|
||||
$hash[$keypair['name']] = $keypair['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!$dry) {
|
||||
// the current structure of attributes is not a terribly good one
|
||||
$this->tree->emitToken($token);
|
||||
}
|
||||
|
||||
if(!$dry && is_int($this->tree->content_model)) {
|
||||
$this->content_model = $this->tree->content_model;
|
||||
$this->tree->content_model = null;
|
||||
|
||||
} elseif($token['type'] === self::ENDTAG) {
|
||||
$this->content_model = self::PCDATA;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3840
thirdparty/html5lib/HTML5/TreeBuilder.php
vendored
Normal file
3840
thirdparty/html5lib/HTML5/TreeBuilder.php
vendored
Normal file
@ -0,0 +1,3840 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
Copyright 2007 Jeroen van der Meer <http://jero.net/>
|
||||
Copyright 2009 Edward Z. Yang <edwardzyang@thewritingpot.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
// Tags for FIX ME!!!: (in order of priority)
|
||||
// XXX - should be fixed NAO!
|
||||
// XERROR - with regards to parse errors
|
||||
// XSCRIPT - with regards to scripting mode
|
||||
// XENCODING - with regards to encoding (for reparsing tests)
|
||||
// XDOM - DOM specific code (tagName is explicitly not marked).
|
||||
// this is not (yet) in helper functions.
|
||||
|
||||
class HTML5_TreeBuilder {
|
||||
public $stack = array();
|
||||
public $content_model;
|
||||
|
||||
private $mode;
|
||||
private $original_mode;
|
||||
private $secondary_mode;
|
||||
private $dom;
|
||||
// Whether or not normal insertion of nodes should actually foster
|
||||
// parent (used in one case in spec)
|
||||
private $foster_parent = false;
|
||||
private $a_formatting = array();
|
||||
|
||||
private $head_pointer = null;
|
||||
private $form_pointer = null;
|
||||
|
||||
private $flag_frameset_ok = true;
|
||||
private $flag_force_quirks = false;
|
||||
private $ignored = false;
|
||||
private $quirks_mode = null;
|
||||
// this gets to 2 when we want to ignore the next lf character, and
|
||||
// is decrement at the beginning of each processed token (this way,
|
||||
// code can check for (bool)$ignore_lf_token, but it phases out
|
||||
// appropriately)
|
||||
private $ignore_lf_token = 0;
|
||||
private $fragment = false;
|
||||
private $root;
|
||||
|
||||
private $scoping = array('applet','button','caption','html','marquee','object','table','td','th', 'svg:foreignObject');
|
||||
private $formatting = array('a','b','big','code','em','font','i','nobr','s','small','strike','strong','tt','u');
|
||||
// dl and ds are speculative
|
||||
private $special = array('address','area','article','aside','base','basefont','bgsound',
|
||||
'blockquote','body','br','center','col','colgroup','command','dc','dd','details','dir','div','dl','ds',
|
||||
'dt','embed','fieldset','figure','footer','form','frame','frameset','h1','h2','h3','h4','h5',
|
||||
'h6','head','header','hgroup','hr','iframe','img','input','isindex','li','link',
|
||||
'listing','menu','meta','nav','noembed','noframes','noscript','ol',
|
||||
'p','param','plaintext','pre','script','select','spacer','style',
|
||||
'tbody','textarea','tfoot','thead','title','tr','ul','wbr');
|
||||
|
||||
private $pendingTableCharacters;
|
||||
private $pendingTableCharactersDirty;
|
||||
|
||||
// Tree construction modes
|
||||
const INITIAL = 0;
|
||||
const BEFORE_HTML = 1;
|
||||
const BEFORE_HEAD = 2;
|
||||
const IN_HEAD = 3;
|
||||
const IN_HEAD_NOSCRIPT = 4;
|
||||
const AFTER_HEAD = 5;
|
||||
const IN_BODY = 6;
|
||||
const IN_CDATA_RCDATA = 7;
|
||||
const IN_TABLE = 8;
|
||||
const IN_TABLE_TEXT = 9;
|
||||
const IN_CAPTION = 10;
|
||||
const IN_COLUMN_GROUP = 11;
|
||||
const IN_TABLE_BODY = 12;
|
||||
const IN_ROW = 13;
|
||||
const IN_CELL = 14;
|
||||
const IN_SELECT = 15;
|
||||
const IN_SELECT_IN_TABLE= 16;
|
||||
const IN_FOREIGN_CONTENT= 17;
|
||||
const AFTER_BODY = 18;
|
||||
const IN_FRAMESET = 19;
|
||||
const AFTER_FRAMESET = 20;
|
||||
const AFTER_AFTER_BODY = 21;
|
||||
const AFTER_AFTER_FRAMESET = 22;
|
||||
|
||||
/**
|
||||
* Converts a magic number to a readable name. Use for debugging.
|
||||
*/
|
||||
private function strConst($number) {
|
||||
static $lookup;
|
||||
if (!$lookup) {
|
||||
$lookup = array();
|
||||
$r = new ReflectionClass('HTML5_TreeBuilder');
|
||||
$consts = $r->getConstants();
|
||||
foreach ($consts as $const => $num) {
|
||||
if (!is_int($num)) continue;
|
||||
$lookup[$num] = $const;
|
||||
}
|
||||
}
|
||||
return $lookup[$number];
|
||||
}
|
||||
|
||||
// The different types of elements.
|
||||
const SPECIAL = 100;
|
||||
const SCOPING = 101;
|
||||
const FORMATTING = 102;
|
||||
const PHRASING = 103;
|
||||
|
||||
// Quirks modes in $quirks_mode
|
||||
const NO_QUIRKS = 200;
|
||||
const QUIRKS_MODE = 201;
|
||||
const LIMITED_QUIRKS_MODE = 202;
|
||||
|
||||
// Marker to be placed in $a_formatting
|
||||
const MARKER = 300;
|
||||
|
||||
// Namespaces for foreign content
|
||||
const NS_HTML = null; // to prevent DOM from requiring NS on everything
|
||||
const NS_MATHML = 'http://www.w3.org/1998/Math/MathML';
|
||||
const NS_SVG = 'http://www.w3.org/2000/svg';
|
||||
const NS_XLINK = 'http://www.w3.org/1999/xlink';
|
||||
const NS_XML = 'http://www.w3.org/XML/1998/namespace';
|
||||
const NS_XMLNS = 'http://www.w3.org/2000/xmlns/';
|
||||
|
||||
// Different types of scopes to test for elements
|
||||
const SCOPE = 0;
|
||||
const SCOPE_LISTITEM = 1;
|
||||
const SCOPE_TABLE = 2;
|
||||
|
||||
public function __construct() {
|
||||
$this->mode = self::INITIAL;
|
||||
$this->dom = new DOMDocument;
|
||||
|
||||
$this->dom->encoding = 'UTF-8';
|
||||
$this->dom->preserveWhiteSpace = true;
|
||||
$this->dom->substituteEntities = true;
|
||||
$this->dom->strictErrorChecking = false;
|
||||
}
|
||||
|
||||
// Process tag tokens
|
||||
public function emitToken($token, $mode = null) {
|
||||
// XXX: ignore parse errors... why are we emitting them, again?
|
||||
if ($token['type'] === HTML5_Tokenizer::PARSEERROR) return;
|
||||
if ($mode === null) $mode = $this->mode;
|
||||
|
||||
/*
|
||||
$backtrace = debug_backtrace();
|
||||
if ($backtrace[1]['class'] !== 'HTML5_TreeBuilder') echo "--\n";
|
||||
echo $this->strConst($mode);
|
||||
if ($this->original_mode) echo " (originally ".$this->strConst($this->original_mode).")";
|
||||
echo "\n ";
|
||||
token_dump($token);
|
||||
$this->printStack();
|
||||
$this->printActiveFormattingElements();
|
||||
if ($this->foster_parent) echo " -> this is a foster parent mode\n";
|
||||
if ($this->flag_frameset_ok) echo " -> frameset ok\n";
|
||||
*/
|
||||
|
||||
if ($this->ignore_lf_token) $this->ignore_lf_token--;
|
||||
$this->ignored = false;
|
||||
// indenting is a little wonky, this can be changed later on
|
||||
switch ($mode) {
|
||||
|
||||
case self::INITIAL:
|
||||
|
||||
/* A character token that is one of U+0009 CHARACTER TABULATION,
|
||||
* U+000A LINE FEED (LF), U+000C FORM FEED (FF), or U+0020 SPACE */
|
||||
if ($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
|
||||
/* Ignore the token. */
|
||||
$this->ignored = true;
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
if (
|
||||
$token['name'] !== 'html' || !empty($token['public']) ||
|
||||
!empty($token['system']) || $token !== 'about:legacy-compat'
|
||||
) {
|
||||
/* If the DOCTYPE token's name is not a case-sensitive match
|
||||
* for the string "html", or if the token's public identifier
|
||||
* is not missing, or if the token's system identifier is
|
||||
* neither missing nor a case-sensitive match for the string
|
||||
* "about:legacy-compat", then there is a parse error (this
|
||||
* is the DOCTYPE parse error). */
|
||||
// DOCTYPE parse error
|
||||
}
|
||||
/* Append a DocumentType node to the Document node, with the name
|
||||
* attribute set to the name given in the DOCTYPE token, or the
|
||||
* empty string if the name was missing; the publicId attribute
|
||||
* set to the public identifier given in the DOCTYPE token, or
|
||||
* the empty string if the public identifier was missing; the
|
||||
* systemId attribute set to the system identifier given in the
|
||||
* DOCTYPE token, or the empty string if the system identifier
|
||||
* was missing; and the other attributes specific to
|
||||
* DocumentType objects set to null and empty lists as
|
||||
* appropriate. Associate the DocumentType node with the
|
||||
* Document object so that it is returned as the value of the
|
||||
* doctype attribute of the Document object. */
|
||||
if (!isset($token['public'])) $token['public'] = null;
|
||||
if (!isset($token['system'])) $token['system'] = null;
|
||||
// XDOM
|
||||
// Yes this is hacky. I'm kind of annoyed that I can't appendChild
|
||||
// a doctype to DOMDocument. Maybe I haven't chanted the right
|
||||
// syllables.
|
||||
$impl = new DOMImplementation();
|
||||
// This call can fail for particularly pathological cases (namely,
|
||||
// the qualifiedName parameter ($token['name']) could be missing.
|
||||
if ($token['name']) {
|
||||
$doctype = $impl->createDocumentType($token['name'], $token['public'], $token['system']);
|
||||
$this->dom->appendChild($doctype);
|
||||
} else {
|
||||
// It looks like libxml's not actually *able* to express this case.
|
||||
// So... don't.
|
||||
$this->dom->emptyDoctype = true;
|
||||
}
|
||||
$public = is_null($token['public']) ? false : strtolower($token['public']);
|
||||
$system = is_null($token['system']) ? false : strtolower($token['system']);
|
||||
$publicStartsWithForQuirks = array(
|
||||
"+//silmaril//dtd html pro v0r11 19970101//",
|
||||
"-//advasoft ltd//dtd html 3.0 aswedit + extensions//",
|
||||
"-//as//dtd html 3.0 aswedit + extensions//",
|
||||
"-//ietf//dtd html 2.0 level 1//",
|
||||
"-//ietf//dtd html 2.0 level 2//",
|
||||
"-//ietf//dtd html 2.0 strict level 1//",
|
||||
"-//ietf//dtd html 2.0 strict level 2//",
|
||||
"-//ietf//dtd html 2.0 strict//",
|
||||
"-//ietf//dtd html 2.0//",
|
||||
"-//ietf//dtd html 2.1e//",
|
||||
"-//ietf//dtd html 3.0//",
|
||||
"-//ietf//dtd html 3.2 final//",
|
||||
"-//ietf//dtd html 3.2//",
|
||||
"-//ietf//dtd html 3//",
|
||||
"-//ietf//dtd html level 0//",
|
||||
"-//ietf//dtd html level 1//",
|
||||
"-//ietf//dtd html level 2//",
|
||||
"-//ietf//dtd html level 3//",
|
||||
"-//ietf//dtd html strict level 0//",
|
||||
"-//ietf//dtd html strict level 1//",
|
||||
"-//ietf//dtd html strict level 2//",
|
||||
"-//ietf//dtd html strict level 3//",
|
||||
"-//ietf//dtd html strict//",
|
||||
"-//ietf//dtd html//",
|
||||
"-//metrius//dtd metrius presentational//",
|
||||
"-//microsoft//dtd internet explorer 2.0 html strict//",
|
||||
"-//microsoft//dtd internet explorer 2.0 html//",
|
||||
"-//microsoft//dtd internet explorer 2.0 tables//",
|
||||
"-//microsoft//dtd internet explorer 3.0 html strict//",
|
||||
"-//microsoft//dtd internet explorer 3.0 html//",
|
||||
"-//microsoft//dtd internet explorer 3.0 tables//",
|
||||
"-//netscape comm. corp.//dtd html//",
|
||||
"-//netscape comm. corp.//dtd strict html//",
|
||||
"-//o'reilly and associates//dtd html 2.0//",
|
||||
"-//o'reilly and associates//dtd html extended 1.0//",
|
||||
"-//o'reilly and associates//dtd html extended relaxed 1.0//",
|
||||
"-//spyglass//dtd html 2.0 extended//",
|
||||
"-//sq//dtd html 2.0 hotmetal + extensions//",
|
||||
"-//sun microsystems corp.//dtd hotjava html//",
|
||||
"-//sun microsystems corp.//dtd hotjava strict html//",
|
||||
"-//w3c//dtd html 3 1995-03-24//",
|
||||
"-//w3c//dtd html 3.2 draft//",
|
||||
"-//w3c//dtd html 3.2 final//",
|
||||
"-//w3c//dtd html 3.2//",
|
||||
"-//w3c//dtd html 3.2s draft//",
|
||||
"-//w3c//dtd html 4.0 frameset//",
|
||||
"-//w3c//dtd html 4.0 transitional//",
|
||||
"-//w3c//dtd html experimental 19960712//",
|
||||
"-//w3c//dtd html experimental 970421//",
|
||||
"-//w3c//dtd w3 html//",
|
||||
"-//w3o//dtd w3 html 3.0//",
|
||||
"-//webtechs//dtd mozilla html 2.0//",
|
||||
"-//webtechs//dtd mozilla html//",
|
||||
);
|
||||
$publicSetToForQuirks = array(
|
||||
"-//w3o//dtd w3 html strict 3.0//",
|
||||
"-/w3c/dtd html 4.0 transitional/en",
|
||||
"html",
|
||||
);
|
||||
$publicStartsWithAndSystemForQuirks = array(
|
||||
"-//w3c//dtd html 4.01 frameset//",
|
||||
"-//w3c//dtd html 4.01 transitional//",
|
||||
);
|
||||
$publicStartsWithForLimitedQuirks = array(
|
||||
"-//w3c//dtd xhtml 1.0 frameset//",
|
||||
"-//w3c//dtd xhtml 1.0 transitional//",
|
||||
);
|
||||
$publicStartsWithAndSystemForLimitedQuirks = array(
|
||||
"-//w3c//dtd html 4.01 frameset//",
|
||||
"-//w3c//dtd html 4.01 transitional//",
|
||||
);
|
||||
// first, do easy checks
|
||||
if (
|
||||
!empty($token['force-quirks']) ||
|
||||
strtolower($token['name']) !== 'html'
|
||||
) {
|
||||
$this->quirks_mode = self::QUIRKS_MODE;
|
||||
} else {
|
||||
do {
|
||||
if ($system) {
|
||||
foreach ($publicStartsWithAndSystemForQuirks as $x) {
|
||||
if (strncmp($public, $x, strlen($x)) === 0) {
|
||||
$this->quirks_mode = self::QUIRKS_MODE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_null($this->quirks_mode)) break;
|
||||
foreach ($publicStartsWithAndSystemForLimitedQuirks as $x) {
|
||||
if (strncmp($public, $x, strlen($x)) === 0) {
|
||||
$this->quirks_mode = self::LIMITED_QUIRKS_MODE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_null($this->quirks_mode)) break;
|
||||
}
|
||||
foreach ($publicSetToForQuirks as $x) {
|
||||
if ($public === $x) {
|
||||
$this->quirks_mode = self::QUIRKS_MODE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_null($this->quirks_mode)) break;
|
||||
foreach ($publicStartsWithForLimitedQuirks as $x) {
|
||||
if (strncmp($public, $x, strlen($x)) === 0) {
|
||||
$this->quirks_mode = self::LIMITED_QUIRKS_MODE;
|
||||
}
|
||||
}
|
||||
if (!is_null($this->quirks_mode)) break;
|
||||
if ($system === "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd") {
|
||||
$this->quirks_mode = self::QUIRKS_MODE;
|
||||
break;
|
||||
}
|
||||
foreach ($publicStartsWithForQuirks as $x) {
|
||||
if (strncmp($public, $x, strlen($x)) === 0) {
|
||||
$this->quirks_mode = self::QUIRKS_MODE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_null($this->quirks_mode)) {
|
||||
$this->quirks_mode = self::NO_QUIRKS;
|
||||
}
|
||||
} while (false);
|
||||
}
|
||||
$this->mode = self::BEFORE_HTML;
|
||||
} else {
|
||||
// parse error
|
||||
/* Switch the insertion mode to "before html", then reprocess the
|
||||
* current token. */
|
||||
$this->mode = self::BEFORE_HTML;
|
||||
$this->quirks_mode = self::QUIRKS_MODE;
|
||||
$this->emitToken($token);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::BEFORE_HTML:
|
||||
|
||||
/* A DOCTYPE token */
|
||||
if($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
// Parse error. Ignore the token.
|
||||
$this->ignored = true;
|
||||
|
||||
/* A comment token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
/* Append a Comment node to the Document object with the data
|
||||
attribute set to the data given in the comment token. */
|
||||
// XDOM
|
||||
$comment = $this->dom->createComment($token['data']);
|
||||
$this->dom->appendChild($comment);
|
||||
|
||||
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
|
||||
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
|
||||
or U+0020 SPACE */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
|
||||
/* Ignore the token. */
|
||||
$this->ignored = true;
|
||||
|
||||
/* A start tag whose tag name is "html" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] == 'html') {
|
||||
/* Create an element for the token in the HTML namespace. Append it
|
||||
* to the Document object. Put this element in the stack of open
|
||||
* elements. */
|
||||
// XDOM
|
||||
$html = $this->insertElement($token, false);
|
||||
$this->dom->appendChild($html);
|
||||
$this->stack[] = $html;
|
||||
|
||||
$this->mode = self::BEFORE_HEAD;
|
||||
|
||||
} else {
|
||||
/* Create an html element. Append it to the Document object. Put
|
||||
* this element in the stack of open elements. */
|
||||
// XDOM
|
||||
$html = $this->dom->createElementNS(self::NS_HTML, 'html');
|
||||
$this->dom->appendChild($html);
|
||||
$this->stack[] = $html;
|
||||
|
||||
/* Switch the insertion mode to "before head", then reprocess the
|
||||
* current token. */
|
||||
$this->mode = self::BEFORE_HEAD;
|
||||
$this->emitToken($token);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::BEFORE_HEAD:
|
||||
|
||||
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
|
||||
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
|
||||
or U+0020 SPACE */
|
||||
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
|
||||
/* Ignore the token. */
|
||||
$this->ignored = true;
|
||||
|
||||
/* A comment token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
/* Append a Comment node to the current node with the data attribute
|
||||
set to the data given in the comment token. */
|
||||
$this->insertComment($token['data']);
|
||||
|
||||
/* A DOCTYPE token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
/* Parse error. Ignore the token */
|
||||
$this->ignored = true;
|
||||
// parse error
|
||||
|
||||
/* A start tag token with the tag name "html" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
|
||||
/* Process the token using the rules for the "in body"
|
||||
* insertion mode. */
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
|
||||
/* A start tag token with the tag name "head" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'head') {
|
||||
/* Insert an HTML element for the token. */
|
||||
$element = $this->insertElement($token);
|
||||
|
||||
/* Set the head element pointer to this new element node. */
|
||||
$this->head_pointer = $element;
|
||||
|
||||
/* Change the insertion mode to "in head". */
|
||||
$this->mode = self::IN_HEAD;
|
||||
|
||||
/* An end tag whose tag name is one of: "head", "body", "html", "br" */
|
||||
} elseif(
|
||||
$token['type'] === HTML5_Tokenizer::ENDTAG && (
|
||||
$token['name'] === 'head' || $token['name'] === 'body' ||
|
||||
$token['name'] === 'html' || $token['name'] === 'br'
|
||||
)) {
|
||||
/* Act as if a start tag token with the tag name "head" and no
|
||||
* attributes had been seen, then reprocess the current token. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'head',
|
||||
'type' => HTML5_Tokenizer::STARTTAG,
|
||||
'attr' => array()
|
||||
));
|
||||
$this->emitToken($token);
|
||||
|
||||
/* Any other end tag */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG) {
|
||||
/* Parse error. Ignore the token. */
|
||||
$this->ignored = true;
|
||||
|
||||
} else {
|
||||
/* Act as if a start tag token with the tag name "head" and no
|
||||
* attributes had been seen, then reprocess the current token.
|
||||
* Note: This will result in an empty head element being
|
||||
* generated, with the current token being reprocessed in the
|
||||
* "after head" insertion mode. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'head',
|
||||
'type' => HTML5_Tokenizer::STARTTAG,
|
||||
'attr' => array()
|
||||
));
|
||||
$this->emitToken($token);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_HEAD:
|
||||
|
||||
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
|
||||
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
|
||||
or U+0020 SPACE. */
|
||||
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
|
||||
/* Insert the character into the current node. */
|
||||
$this->insertText($token['data']);
|
||||
|
||||
/* A comment token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
/* Append a Comment node to the current node with the data attribute
|
||||
set to the data given in the comment token. */
|
||||
$this->insertComment($token['data']);
|
||||
|
||||
/* A DOCTYPE token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
/* Parse error. Ignore the token. */
|
||||
$this->ignored = true;
|
||||
// parse error
|
||||
|
||||
/* A start tag whose tag name is "html" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
$token['name'] === 'html') {
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
|
||||
/* A start tag whose tag name is one of: "base", "command", "link" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
($token['name'] === 'base' || $token['name'] === 'command' ||
|
||||
$token['name'] === 'link')) {
|
||||
/* Insert an HTML element for the token. Immediately pop the
|
||||
* current node off the stack of open elements. */
|
||||
$this->insertElement($token);
|
||||
array_pop($this->stack);
|
||||
|
||||
// YYY: Acknowledge the token's self-closing flag, if it is set.
|
||||
|
||||
/* A start tag whose tag name is "meta" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'meta') {
|
||||
/* Insert an HTML element for the token. Immediately pop the
|
||||
* current node off the stack of open elements. */
|
||||
$this->insertElement($token);
|
||||
array_pop($this->stack);
|
||||
|
||||
// XERROR: Acknowledge the token's self-closing flag, if it is set.
|
||||
|
||||
// XENCODING: If the element has a charset attribute, and its value is a
|
||||
// supported encoding, and the confidence is currently tentative,
|
||||
// then change the encoding to the encoding given by the value of
|
||||
// the charset attribute.
|
||||
//
|
||||
// Otherwise, if the element has a content attribute, and applying
|
||||
// the algorithm for extracting an encoding from a Content-Type to
|
||||
// its value returns a supported encoding encoding, and the
|
||||
// confidence is currently tentative, then change the encoding to
|
||||
// the encoding encoding.
|
||||
|
||||
/* A start tag with the tag name "title" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'title') {
|
||||
$this->insertRCDATAElement($token);
|
||||
|
||||
/* A start tag whose tag name is "noscript", if the scripting flag is enabled, or
|
||||
* A start tag whose tag name is one of: "noframes", "style" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
($token['name'] === 'noscript' || $token['name'] === 'noframes' || $token['name'] === 'style')) {
|
||||
// XSCRIPT: Scripting flag not respected
|
||||
$this->insertCDATAElement($token);
|
||||
|
||||
// XSCRIPT: Scripting flag disable not implemented
|
||||
|
||||
/* A start tag with the tag name "script" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'script') {
|
||||
/* 1. Create an element for the token in the HTML namespace. */
|
||||
$node = $this->insertElement($token, false);
|
||||
|
||||
/* 2. Mark the element as being "parser-inserted" */
|
||||
// Uhhh... XSCRIPT
|
||||
|
||||
/* 3. If the parser was originally created for the HTML
|
||||
* fragment parsing algorithm, then mark the script element as
|
||||
* "already executed". (fragment case) */
|
||||
// ditto... XSCRIPT
|
||||
|
||||
/* 4. Append the new element to the current node and push it onto
|
||||
* the stack of open elements. */
|
||||
end($this->stack)->appendChild($node);
|
||||
$this->stack[] = $node;
|
||||
// I guess we could squash these together
|
||||
|
||||
/* 6. Let the original insertion mode be the current insertion mode. */
|
||||
$this->original_mode = $this->mode;
|
||||
/* 7. Switch the insertion mode to "in CDATA/RCDATA" */
|
||||
$this->mode = self::IN_CDATA_RCDATA;
|
||||
/* 5. Switch the tokeniser's content model flag to the CDATA state. */
|
||||
$this->content_model = HTML5_Tokenizer::CDATA;
|
||||
|
||||
/* An end tag with the tag name "head" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'head') {
|
||||
/* Pop the current node (which will be the head element) off the stack of open elements. */
|
||||
array_pop($this->stack);
|
||||
|
||||
/* Change the insertion mode to "after head". */
|
||||
$this->mode = self::AFTER_HEAD;
|
||||
|
||||
// Slight logic inversion here to minimize duplication
|
||||
/* A start tag with the tag name "head". */
|
||||
/* An end tag whose tag name is not one of: "body", "html", "br" */
|
||||
} elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'head') ||
|
||||
($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] !== 'html' &&
|
||||
$token['name'] !== 'body' && $token['name'] !== 'br')) {
|
||||
// Parse error. Ignore the token.
|
||||
$this->ignored = true;
|
||||
|
||||
/* Anything else */
|
||||
} else {
|
||||
/* Act as if an end tag token with the tag name "head" had been
|
||||
* seen, and reprocess the current token. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'head',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
|
||||
/* Then, reprocess the current token. */
|
||||
$this->emitToken($token);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_HEAD_NOSCRIPT:
|
||||
if ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
// parse error
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'noscript') {
|
||||
/* Pop the current node (which will be a noscript element) from the
|
||||
* stack of open elements; the new current node will be a head
|
||||
* element. */
|
||||
array_pop($this->stack);
|
||||
$this->mode = self::IN_HEAD;
|
||||
} elseif (
|
||||
($token['type'] === HTML5_Tokenizer::SPACECHARACTER) ||
|
||||
($token['type'] === HTML5_Tokenizer::COMMENT) ||
|
||||
($token['type'] === HTML5_Tokenizer::STARTTAG && (
|
||||
$token['name'] === 'link' || $token['name'] === 'meta' ||
|
||||
$token['name'] === 'noframes' || $token['name'] === 'style'))) {
|
||||
$this->processWithRulesFor($token, self::IN_HEAD);
|
||||
// inverted logic
|
||||
} elseif (
|
||||
($token['type'] === HTML5_Tokenizer::STARTTAG && (
|
||||
$token['name'] === 'head' || $token['name'] === 'noscript')) ||
|
||||
($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
$token['name'] !== 'br')) {
|
||||
// parse error
|
||||
} else {
|
||||
// parse error
|
||||
$this->emitToken(array(
|
||||
'type' => HTML5_Tokenizer::ENDTAG,
|
||||
'name' => 'noscript',
|
||||
));
|
||||
$this->emitToken($token);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::AFTER_HEAD:
|
||||
/* Handle the token as follows: */
|
||||
|
||||
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
|
||||
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
|
||||
or U+0020 SPACE */
|
||||
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
|
||||
/* Append the character to the current node. */
|
||||
$this->insertText($token['data']);
|
||||
|
||||
/* A comment token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
/* Append a Comment node to the current node with the data attribute
|
||||
set to the data given in the comment token. */
|
||||
$this->insertComment($token['data']);
|
||||
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
// parse error
|
||||
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
|
||||
/* A start tag token with the tag name "body" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'body') {
|
||||
$this->insertElement($token);
|
||||
|
||||
/* Set the frameset-ok flag to "not ok". */
|
||||
$this->flag_frameset_ok = false;
|
||||
|
||||
/* Change the insertion mode to "in body". */
|
||||
$this->mode = self::IN_BODY;
|
||||
|
||||
/* A start tag token with the tag name "frameset" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'frameset') {
|
||||
/* Insert a frameset element for the token. */
|
||||
$this->insertElement($token);
|
||||
|
||||
/* Change the insertion mode to "in frameset". */
|
||||
$this->mode = self::IN_FRAMESET;
|
||||
|
||||
/* A start tag token whose tag name is one of: "base", "link", "meta",
|
||||
"script", "style", "title" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
|
||||
array('base', 'link', 'meta', 'noframes', 'script', 'style', 'title'))) {
|
||||
// parse error
|
||||
/* Push the node pointed to by the head element pointer onto the
|
||||
* stack of open elements. */
|
||||
$this->stack[] = $this->head_pointer;
|
||||
$this->processWithRulesFor($token, self::IN_HEAD);
|
||||
array_splice($this->stack, array_search($this->head_pointer, $this->stack, true), 1);
|
||||
|
||||
// inversion of specification
|
||||
} elseif(
|
||||
($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'head') ||
|
||||
($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
$token['name'] !== 'body' && $token['name'] !== 'html' &&
|
||||
$token['name'] !== 'br')) {
|
||||
// parse error
|
||||
|
||||
/* Anything else */
|
||||
} else {
|
||||
$this->emitToken(array(
|
||||
'name' => 'body',
|
||||
'type' => HTML5_Tokenizer::STARTTAG,
|
||||
'attr' => array()
|
||||
));
|
||||
$this->flag_frameset_ok = true;
|
||||
$this->emitToken($token);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_BODY:
|
||||
/* Handle the token as follows: */
|
||||
|
||||
switch($token['type']) {
|
||||
/* A character token */
|
||||
case HTML5_Tokenizer::CHARACTER:
|
||||
case HTML5_Tokenizer::SPACECHARACTER:
|
||||
/* Reconstruct the active formatting elements, if any. */
|
||||
$this->reconstructActiveFormattingElements();
|
||||
|
||||
/* Append the token's character to the current node. */
|
||||
$this->insertText($token['data']);
|
||||
|
||||
/* If the token is not one of U+0009 CHARACTER TABULATION,
|
||||
* U+000A LINE FEED (LF), U+000C FORM FEED (FF), or U+0020
|
||||
* SPACE, then set the frameset-ok flag to "not ok". */
|
||||
// i.e., if any of the characters is not whitespace
|
||||
if (strlen($token['data']) !== strspn($token['data'], HTML5_Tokenizer::WHITESPACE)) {
|
||||
$this->flag_frameset_ok = false;
|
||||
}
|
||||
break;
|
||||
|
||||
/* A comment token */
|
||||
case HTML5_Tokenizer::COMMENT:
|
||||
/* Append a Comment node to the current node with the data
|
||||
attribute set to the data given in the comment token. */
|
||||
$this->insertComment($token['data']);
|
||||
break;
|
||||
|
||||
case HTML5_Tokenizer::DOCTYPE:
|
||||
// parse error
|
||||
break;
|
||||
|
||||
case HTML5_Tokenizer::EOF:
|
||||
// parse error
|
||||
break;
|
||||
|
||||
case HTML5_Tokenizer::STARTTAG:
|
||||
switch($token['name']) {
|
||||
case 'html':
|
||||
// parse error
|
||||
/* For each attribute on the token, check to see if the
|
||||
* attribute is already present on the top element of the
|
||||
* stack of open elements. If it is not, add the attribute
|
||||
* and its corresponding value to that element. */
|
||||
foreach($token['attr'] as $attr) {
|
||||
if(!$this->stack[0]->hasAttribute($attr['name'])) {
|
||||
$this->stack[0]->setAttribute($attr['name'], $attr['value']);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'base': case 'command': case 'link': case 'meta': case 'noframes':
|
||||
case 'script': case 'style': case 'title':
|
||||
/* Process the token as if the insertion mode had been "in
|
||||
head". */
|
||||
$this->processWithRulesFor($token, self::IN_HEAD);
|
||||
break;
|
||||
|
||||
/* A start tag token with the tag name "body" */
|
||||
case 'body':
|
||||
/* Parse error. If the second element on the stack of open
|
||||
elements is not a body element, or, if the stack of open
|
||||
elements has only one node on it, then ignore the token.
|
||||
(fragment case) */
|
||||
if(count($this->stack) === 1 || $this->stack[1]->tagName !== 'body') {
|
||||
$this->ignored = true;
|
||||
// Ignore
|
||||
|
||||
/* Otherwise, for each attribute on the token, check to see
|
||||
if the attribute is already present on the body element (the
|
||||
second element) on the stack of open elements. If it is not,
|
||||
add the attribute and its corresponding value to that
|
||||
element. */
|
||||
} else {
|
||||
foreach($token['attr'] as $attr) {
|
||||
if(!$this->stack[1]->hasAttribute($attr['name'])) {
|
||||
$this->stack[1]->setAttribute($attr['name'], $attr['value']);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'frameset':
|
||||
// parse error
|
||||
/* If the second element on the stack of open elements is
|
||||
* not a body element, or, if the stack of open elements
|
||||
* has only one node on it, then ignore the token.
|
||||
* (fragment case) */
|
||||
if(count($this->stack) === 1 || $this->stack[1]->tagName !== 'body') {
|
||||
$this->ignored = true;
|
||||
// Ignore
|
||||
} elseif (!$this->flag_frameset_ok) {
|
||||
$this->ignored = true;
|
||||
// Ignore
|
||||
} else {
|
||||
/* 1. Remove the second element on the stack of open
|
||||
* elements from its parent node, if it has one. */
|
||||
if($this->stack[1]->parentNode) {
|
||||
$this->stack[1]->parentNode->removeChild($this->stack[1]);
|
||||
}
|
||||
|
||||
/* 2. Pop all the nodes from the bottom of the stack of
|
||||
* open elements, from the current node up to the root
|
||||
* html element. */
|
||||
array_splice($this->stack, 1);
|
||||
|
||||
$this->insertElement($token);
|
||||
$this->mode = self::IN_FRAMESET;
|
||||
}
|
||||
break;
|
||||
|
||||
// in spec, there is a diversion here
|
||||
|
||||
case 'address': case 'article': case 'aside': case 'blockquote':
|
||||
case 'center': case 'datagrid': case 'details': case 'dir':
|
||||
case 'div': case 'dl': case 'fieldset': case 'figure': case 'footer':
|
||||
case 'header': case 'hgroup': case 'menu': case 'nav':
|
||||
case 'ol': case 'p': case 'section': case 'ul':
|
||||
/* If the stack of open elements has a p element in scope,
|
||||
then act as if an end tag with the tag name p had been
|
||||
seen. */
|
||||
if($this->elementInScope('p')) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'p',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
break;
|
||||
|
||||
/* A start tag whose tag name is one of: "h1", "h2", "h3", "h4",
|
||||
"h5", "h6" */
|
||||
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
|
||||
/* If the stack of open elements has a p element in scope,
|
||||
then act as if an end tag with the tag name p had been seen. */
|
||||
if($this->elementInScope('p')) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'p',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* If the current node is an element whose tag name is one
|
||||
* of "h1", "h2", "h3", "h4", "h5", or "h6", then this is a
|
||||
* parse error; pop the current node off the stack of open
|
||||
* elements. */
|
||||
$peek = array_pop($this->stack);
|
||||
if (in_array($peek->tagName, array("h1", "h2", "h3", "h4", "h5", "h6"))) {
|
||||
// parse error
|
||||
} else {
|
||||
$this->stack[] = $peek;
|
||||
}
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
break;
|
||||
|
||||
case 'pre': case 'listing':
|
||||
/* If the stack of open elements has a p element in scope,
|
||||
then act as if an end tag with the tag name p had been seen. */
|
||||
if($this->elementInScope('p')) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'p',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
$this->insertElement($token);
|
||||
/* If the next token is a U+000A LINE FEED (LF) character
|
||||
* token, then ignore that token and move on to the next
|
||||
* one. (Newlines at the start of pre blocks are ignored as
|
||||
* an authoring convenience.) */
|
||||
$this->ignore_lf_token = 2;
|
||||
$this->flag_frameset_ok = false;
|
||||
break;
|
||||
|
||||
/* A start tag whose tag name is "form" */
|
||||
case 'form':
|
||||
/* If the form element pointer is not null, ignore the
|
||||
token with a parse error. */
|
||||
if($this->form_pointer !== null) {
|
||||
$this->ignored = true;
|
||||
// Ignore.
|
||||
|
||||
/* Otherwise: */
|
||||
} else {
|
||||
/* If the stack of open elements has a p element in
|
||||
scope, then act as if an end tag with the tag name p
|
||||
had been seen. */
|
||||
if($this->elementInScope('p')) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'p',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* Insert an HTML element for the token, and set the
|
||||
form element pointer to point to the element created. */
|
||||
$element = $this->insertElement($token);
|
||||
$this->form_pointer = $element;
|
||||
}
|
||||
break;
|
||||
|
||||
// condensed specification
|
||||
case 'li': case 'dc': case 'dd': case 'ds': case 'dt':
|
||||
/* 1. Set the frameset-ok flag to "not ok". */
|
||||
$this->flag_frameset_ok = false;
|
||||
|
||||
$stack_length = count($this->stack) - 1;
|
||||
for($n = $stack_length; 0 <= $n; $n--) {
|
||||
/* 2. Initialise node to be the current node (the
|
||||
bottommost node of the stack). */
|
||||
$stop = false;
|
||||
$node = $this->stack[$n];
|
||||
$cat = $this->getElementCategory($node);
|
||||
|
||||
// for case 'li':
|
||||
/* 3. If node is an li element, then act as if an end
|
||||
* tag with the tag name "li" had been seen, then jump
|
||||
* to the last step. */
|
||||
// for case 'dc': case 'dd': case 'ds': case 'dt':
|
||||
/* If node is a dc, dd, ds or dt element, then act as if an end
|
||||
* tag with the same tag name as node had been seen, then
|
||||
* jump to the last step. */
|
||||
if(($token['name'] === 'li' && $node->tagName === 'li') ||
|
||||
($token['name'] !== 'li' && ($node->tagName == 'dc' || $node->tagName === 'dd' || $node->tagName == 'ds' || $node->tagName === 'dt'))) { // limited conditional
|
||||
$this->emitToken(array(
|
||||
'type' => HTML5_Tokenizer::ENDTAG,
|
||||
'name' => $node->tagName,
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
/* 4. If node is not in the formatting category, and is
|
||||
not in the phrasing category, and is not an address,
|
||||
div or p element, then stop this algorithm. */
|
||||
if($cat !== self::FORMATTING && $cat !== self::PHRASING &&
|
||||
$node->tagName !== 'address' && $node->tagName !== 'div' &&
|
||||
$node->tagName !== 'p') {
|
||||
break;
|
||||
}
|
||||
|
||||
/* 5. Otherwise, set node to the previous entry in the
|
||||
* stack of open elements and return to step 2. */
|
||||
}
|
||||
|
||||
/* 6. This is the last step. */
|
||||
|
||||
/* If the stack of open elements has a p element in scope,
|
||||
then act as if an end tag with the tag name p had been
|
||||
seen. */
|
||||
if($this->elementInScope('p')) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'p',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* Finally, insert an HTML element with the same tag
|
||||
name as the token's. */
|
||||
$this->insertElement($token);
|
||||
break;
|
||||
|
||||
/* A start tag token whose tag name is "plaintext" */
|
||||
case 'plaintext':
|
||||
/* If the stack of open elements has a p element in scope,
|
||||
then act as if an end tag with the tag name p had been
|
||||
seen. */
|
||||
if($this->elementInScope('p')) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'p',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
|
||||
$this->content_model = HTML5_Tokenizer::PLAINTEXT;
|
||||
break;
|
||||
|
||||
// more diversions
|
||||
|
||||
/* A start tag whose tag name is "a" */
|
||||
case 'a':
|
||||
/* If the list of active formatting elements contains
|
||||
an element whose tag name is "a" between the end of the
|
||||
list and the last marker on the list (or the start of
|
||||
the list if there is no marker on the list), then this
|
||||
is a parse error; act as if an end tag with the tag name
|
||||
"a" had been seen, then remove that element from the list
|
||||
of active formatting elements and the stack of open
|
||||
elements if the end tag didn't already remove it (it
|
||||
might not have if the element is not in table scope). */
|
||||
$leng = count($this->a_formatting);
|
||||
|
||||
for($n = $leng - 1; $n >= 0; $n--) {
|
||||
if($this->a_formatting[$n] === self::MARKER) {
|
||||
break;
|
||||
|
||||
} elseif($this->a_formatting[$n]->tagName === 'a') {
|
||||
$a = $this->a_formatting[$n];
|
||||
$this->emitToken(array(
|
||||
'name' => 'a',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
if (in_array($a, $this->a_formatting)) {
|
||||
$a_i = array_search($a, $this->a_formatting, true);
|
||||
if($a_i !== false) array_splice($this->a_formatting, $a_i, 1);
|
||||
}
|
||||
if (in_array($a, $this->stack)) {
|
||||
$a_i = array_search($a, $this->stack, true);
|
||||
if ($a_i !== false) array_splice($this->stack, $a_i, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reconstruct the active formatting elements, if any. */
|
||||
$this->reconstructActiveFormattingElements();
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$el = $this->insertElement($token);
|
||||
|
||||
/* Add that element to the list of active formatting
|
||||
elements. */
|
||||
$this->a_formatting[] = $el;
|
||||
break;
|
||||
|
||||
case 'b': case 'big': case 'code': case 'em': case 'font': case 'i':
|
||||
case 's': case 'small': case 'strike':
|
||||
case 'strong': case 'tt': case 'u':
|
||||
/* Reconstruct the active formatting elements, if any. */
|
||||
$this->reconstructActiveFormattingElements();
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$el = $this->insertElement($token);
|
||||
|
||||
/* Add that element to the list of active formatting
|
||||
elements. */
|
||||
$this->a_formatting[] = $el;
|
||||
break;
|
||||
|
||||
case 'nobr':
|
||||
/* Reconstruct the active formatting elements, if any. */
|
||||
$this->reconstructActiveFormattingElements();
|
||||
|
||||
/* If the stack of open elements has a nobr element in
|
||||
* scope, then this is a parse error; act as if an end tag
|
||||
* with the tag name "nobr" had been seen, then once again
|
||||
* reconstruct the active formatting elements, if any. */
|
||||
if ($this->elementInScope('nobr')) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'nobr',
|
||||
'type' => HTML5_Tokenizer::ENDTAG,
|
||||
));
|
||||
$this->reconstructActiveFormattingElements();
|
||||
}
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$el = $this->insertElement($token);
|
||||
|
||||
/* Add that element to the list of active formatting
|
||||
elements. */
|
||||
$this->a_formatting[] = $el;
|
||||
break;
|
||||
|
||||
// another diversion
|
||||
|
||||
/* A start tag token whose tag name is "button" */
|
||||
case 'button':
|
||||
/* If the stack of open elements has a button element in scope,
|
||||
then this is a parse error; act as if an end tag with the tag
|
||||
name "button" had been seen, then reprocess the token. (We don't
|
||||
do that. Unnecessary.) (I hope you're right! -- ezyang) */
|
||||
if($this->elementInScope('button')) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'button',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* Reconstruct the active formatting elements, if any. */
|
||||
$this->reconstructActiveFormattingElements();
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
|
||||
/* Insert a marker at the end of the list of active
|
||||
formatting elements. */
|
||||
$this->a_formatting[] = self::MARKER;
|
||||
|
||||
$this->flag_frameset_ok = false;
|
||||
break;
|
||||
|
||||
case 'applet': case 'marquee': case 'object':
|
||||
/* Reconstruct the active formatting elements, if any. */
|
||||
$this->reconstructActiveFormattingElements();
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
|
||||
/* Insert a marker at the end of the list of active
|
||||
formatting elements. */
|
||||
$this->a_formatting[] = self::MARKER;
|
||||
|
||||
$this->flag_frameset_ok = false;
|
||||
break;
|
||||
|
||||
// spec diversion
|
||||
|
||||
/* A start tag whose tag name is "table" */
|
||||
case 'table':
|
||||
/* If the Document is not set to quirks mode, and the
|
||||
* stack of open elements has a p element in scope, then
|
||||
* act as if an end tag with the tag name "p" had been
|
||||
* seen. */
|
||||
if($this->quirks_mode !== self::QUIRKS_MODE &&
|
||||
$this->elementInScope('p')) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'p',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
|
||||
$this->flag_frameset_ok = false;
|
||||
|
||||
/* Change the insertion mode to "in table". */
|
||||
$this->mode = self::IN_TABLE;
|
||||
break;
|
||||
|
||||
/* A start tag whose tag name is one of: "area", "basefont",
|
||||
"bgsound", "br", "embed", "img", "param", "spacer", "wbr" */
|
||||
case 'area': case 'basefont': case 'bgsound': case 'br':
|
||||
case 'embed': case 'img': case 'input': case 'keygen': case 'spacer':
|
||||
case 'wbr':
|
||||
/* Reconstruct the active formatting elements, if any. */
|
||||
$this->reconstructActiveFormattingElements();
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
|
||||
/* Immediately pop the current node off the stack of open elements. */
|
||||
array_pop($this->stack);
|
||||
|
||||
// YYY: Acknowledge the token's self-closing flag, if it is set.
|
||||
|
||||
$this->flag_frameset_ok = false;
|
||||
break;
|
||||
|
||||
case 'param': case 'source':
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
|
||||
/* Immediately pop the current node off the stack of open elements. */
|
||||
array_pop($this->stack);
|
||||
|
||||
// YYY: Acknowledge the token's self-closing flag, if it is set.
|
||||
break;
|
||||
|
||||
/* A start tag whose tag name is "hr" */
|
||||
case 'hr':
|
||||
/* If the stack of open elements has a p element in scope,
|
||||
then act as if an end tag with the tag name p had been seen. */
|
||||
if($this->elementInScope('p')) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'p',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
|
||||
/* Immediately pop the current node off the stack of open elements. */
|
||||
array_pop($this->stack);
|
||||
|
||||
// YYY: Acknowledge the token's self-closing flag, if it is set.
|
||||
|
||||
$this->flag_frameset_ok = false;
|
||||
break;
|
||||
|
||||
/* A start tag whose tag name is "image" */
|
||||
case 'image':
|
||||
/* Parse error. Change the token's tag name to "img" and
|
||||
reprocess it. (Don't ask.) */
|
||||
$token['name'] = 'img';
|
||||
$this->emitToken($token);
|
||||
break;
|
||||
|
||||
/* A start tag whose tag name is "isindex" */
|
||||
case 'isindex':
|
||||
/* Parse error. */
|
||||
|
||||
/* If the form element pointer is not null,
|
||||
then ignore the token. */
|
||||
if($this->form_pointer === null) {
|
||||
/* Act as if a start tag token with the tag name "form" had
|
||||
been seen. */
|
||||
/* If the token has an attribute called "action", set
|
||||
* the action attribute on the resulting form
|
||||
* element to the value of the "action" attribute of
|
||||
* the token. */
|
||||
$attr = array();
|
||||
$action = $this->getAttr($token, 'action');
|
||||
if ($action !== false) {
|
||||
$attr[] = array('name' => 'action', 'value' => $action);
|
||||
}
|
||||
$this->emitToken(array(
|
||||
'name' => 'form',
|
||||
'type' => HTML5_Tokenizer::STARTTAG,
|
||||
'attr' => $attr
|
||||
));
|
||||
|
||||
/* Act as if a start tag token with the tag name "hr" had
|
||||
been seen. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'hr',
|
||||
'type' => HTML5_Tokenizer::STARTTAG,
|
||||
'attr' => array()
|
||||
));
|
||||
|
||||
/* Act as if a start tag token with the tag name "label"
|
||||
had been seen. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'label',
|
||||
'type' => HTML5_Tokenizer::STARTTAG,
|
||||
'attr' => array()
|
||||
));
|
||||
|
||||
/* Act as if a stream of character tokens had been seen. */
|
||||
$prompt = $this->getAttr($token, 'prompt');
|
||||
if ($prompt === false) {
|
||||
$prompt = 'This is a searchable index. '.
|
||||
'Insert your search keywords here: ';
|
||||
}
|
||||
$this->emitToken(array(
|
||||
'data' => $prompt,
|
||||
'type' => HTML5_Tokenizer::CHARACTER,
|
||||
));
|
||||
|
||||
/* Act as if a start tag token with the tag name "input"
|
||||
had been seen, with all the attributes from the "isindex"
|
||||
token, except with the "name" attribute set to the value
|
||||
"isindex" (ignoring any explicit "name" attribute). */
|
||||
$attr = array();
|
||||
foreach ($token['attr'] as $keypair) {
|
||||
if ($keypair['name'] === 'name' || $keypair['name'] === 'action' ||
|
||||
$keypair['name'] === 'prompt') continue;
|
||||
$attr[] = $keypair;
|
||||
}
|
||||
$attr[] = array('name' => 'name', 'value' => 'isindex');
|
||||
|
||||
$this->emitToken(array(
|
||||
'name' => 'input',
|
||||
'type' => HTML5_Tokenizer::STARTTAG,
|
||||
'attr' => $attr
|
||||
));
|
||||
|
||||
/* Act as if an end tag token with the tag name "label"
|
||||
had been seen. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'label',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
|
||||
/* Act as if a start tag token with the tag name "hr" had
|
||||
been seen. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'hr',
|
||||
'type' => HTML5_Tokenizer::STARTTAG
|
||||
));
|
||||
|
||||
/* Act as if an end tag token with the tag name "form" had
|
||||
been seen. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'form',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
} else {
|
||||
$this->ignored = true;
|
||||
}
|
||||
break;
|
||||
|
||||
/* A start tag whose tag name is "textarea" */
|
||||
case 'textarea':
|
||||
$this->insertElement($token);
|
||||
|
||||
/* If the next token is a U+000A LINE FEED (LF)
|
||||
* character token, then ignore that token and move on to
|
||||
* the next one. (Newlines at the start of textarea
|
||||
* elements are ignored as an authoring convenience.)
|
||||
* need flag, see also <pre> */
|
||||
$this->ignore_lf_token = 2;
|
||||
|
||||
$this->original_mode = $this->mode;
|
||||
$this->flag_frameset_ok = false;
|
||||
$this->mode = self::IN_CDATA_RCDATA;
|
||||
|
||||
/* Switch the tokeniser's content model flag to the
|
||||
RCDATA state. */
|
||||
$this->content_model = HTML5_Tokenizer::RCDATA;
|
||||
break;
|
||||
|
||||
/* A start tag token whose tag name is "xmp" */
|
||||
case 'xmp':
|
||||
/* If the stack of open elements has a p element in
|
||||
scope, then act as if an end tag with the tag name
|
||||
"p" has been seen. */
|
||||
if ($this->elementInScope('p')) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'p',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* Reconstruct the active formatting elements, if any. */
|
||||
$this->reconstructActiveFormattingElements();
|
||||
|
||||
$this->flag_frameset_ok = false;
|
||||
|
||||
$this->insertCDATAElement($token);
|
||||
break;
|
||||
|
||||
case 'iframe':
|
||||
$this->flag_frameset_ok = false;
|
||||
$this->insertCDATAElement($token);
|
||||
break;
|
||||
|
||||
case 'noembed': case 'noscript':
|
||||
// XSCRIPT: should check scripting flag
|
||||
$this->insertCDATAElement($token);
|
||||
break;
|
||||
|
||||
/* A start tag whose tag name is "select" */
|
||||
case 'select':
|
||||
/* Reconstruct the active formatting elements, if any. */
|
||||
$this->reconstructActiveFormattingElements();
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
|
||||
$this->flag_frameset_ok = false;
|
||||
|
||||
/* If the insertion mode is one of in table", "in caption",
|
||||
* "in column group", "in table body", "in row", or "in
|
||||
* cell", then switch the insertion mode to "in select in
|
||||
* table". Otherwise, switch the insertion mode to "in
|
||||
* select". */
|
||||
if (
|
||||
$this->mode === self::IN_TABLE || $this->mode === self::IN_CAPTION ||
|
||||
$this->mode === self::IN_COLUMN_GROUP || $this->mode ==+self::IN_TABLE_BODY ||
|
||||
$this->mode === self::IN_ROW || $this->mode === self::IN_CELL
|
||||
) {
|
||||
$this->mode = self::IN_SELECT_IN_TABLE;
|
||||
} else {
|
||||
$this->mode = self::IN_SELECT;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'option': case 'optgroup':
|
||||
if ($this->elementInScope('option')) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'option',
|
||||
'type' => HTML5_Tokenizer::ENDTAG,
|
||||
));
|
||||
}
|
||||
$this->reconstructActiveFormattingElements();
|
||||
$this->insertElement($token);
|
||||
break;
|
||||
|
||||
case 'rp': case 'rt':
|
||||
/* If the stack of open elements has a ruby element in scope, then generate
|
||||
* implied end tags. If the current node is not then a ruby element, this is
|
||||
* a parse error; pop all the nodes from the current node up to the node
|
||||
* immediately before the bottommost ruby element on the stack of open elements.
|
||||
*/
|
||||
if ($this->elementInScope('ruby')) {
|
||||
$this->generateImpliedEndTags();
|
||||
}
|
||||
$peek = false;
|
||||
do {
|
||||
if ($peek) {
|
||||
// parse error
|
||||
}
|
||||
$peek = array_pop($this->stack);
|
||||
} while ($peek->tagName !== 'ruby');
|
||||
$this->stack[] = $peek; // we popped one too many
|
||||
$this->insertElement($token);
|
||||
break;
|
||||
|
||||
// spec diversion
|
||||
|
||||
case 'math':
|
||||
$this->reconstructActiveFormattingElements();
|
||||
$token = $this->adjustMathMLAttributes($token);
|
||||
$token = $this->adjustForeignAttributes($token);
|
||||
$this->insertForeignElement($token, self::NS_MATHML);
|
||||
if (isset($token['self-closing'])) {
|
||||
// XERROR: acknowledge the token's self-closing flag
|
||||
array_pop($this->stack);
|
||||
}
|
||||
if ($this->mode !== self::IN_FOREIGN_CONTENT) {
|
||||
$this->secondary_mode = $this->mode;
|
||||
$this->mode = self::IN_FOREIGN_CONTENT;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'svg':
|
||||
$this->reconstructActiveFormattingElements();
|
||||
$token = $this->adjustSVGAttributes($token);
|
||||
$token = $this->adjustForeignAttributes($token);
|
||||
$this->insertForeignElement($token, self::NS_SVG);
|
||||
if (isset($token['self-closing'])) {
|
||||
// XERROR: acknowledge the token's self-closing flag
|
||||
array_pop($this->stack);
|
||||
}
|
||||
if ($this->mode !== self::IN_FOREIGN_CONTENT) {
|
||||
$this->secondary_mode = $this->mode;
|
||||
$this->mode = self::IN_FOREIGN_CONTENT;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'caption': case 'col': case 'colgroup': case 'frame': case 'head':
|
||||
case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead': case 'tr':
|
||||
// parse error
|
||||
break;
|
||||
|
||||
/* A start tag token not covered by the previous entries */
|
||||
default:
|
||||
/* Reconstruct the active formatting elements, if any. */
|
||||
$this->reconstructActiveFormattingElements();
|
||||
|
||||
$this->insertElement($token);
|
||||
/* This element will be a phrasing element. */
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case HTML5_Tokenizer::ENDTAG:
|
||||
switch($token['name']) {
|
||||
/* An end tag with the tag name "body" */
|
||||
case 'body':
|
||||
/* If the stack of open elements does not have a body
|
||||
* element in scope, this is a parse error; ignore the
|
||||
* token. */
|
||||
if(!$this->elementInScope('body')) {
|
||||
$this->ignored = true;
|
||||
|
||||
/* Otherwise, if there is a node in the stack of open
|
||||
* elements that is not either a dc element, a dd element,
|
||||
* a ds element, a dt element, an li element, an optgroup
|
||||
* element, an option element, a p element, an rp element,
|
||||
* an rt element, a tbody element, a td element, a tfoot
|
||||
* element, a th element, a thead element, a tr element,
|
||||
* the body element, or the html element, then this is a
|
||||
* parse error.
|
||||
*/
|
||||
} else {
|
||||
// XERROR: implement this check for parse error
|
||||
}
|
||||
|
||||
/* Change the insertion mode to "after body". */
|
||||
$this->mode = self::AFTER_BODY;
|
||||
break;
|
||||
|
||||
/* An end tag with the tag name "html" */
|
||||
case 'html':
|
||||
/* Act as if an end tag with tag name "body" had been seen,
|
||||
then, if that token wasn't ignored, reprocess the current
|
||||
token. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'body',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
|
||||
if (!$this->ignored) $this->emitToken($token);
|
||||
break;
|
||||
|
||||
case 'address': case 'article': case 'aside': case 'blockquote':
|
||||
case 'center': case 'datagrid': case 'details': case 'dir':
|
||||
case 'div': case 'dl': case 'fieldset': case 'footer':
|
||||
case 'header': case 'hgroup': case 'listing': case 'menu':
|
||||
case 'nav': case 'ol': case 'pre': case 'section': case 'ul':
|
||||
/* If the stack of open elements has an element in scope
|
||||
with the same tag name as that of the token, then generate
|
||||
implied end tags. */
|
||||
if($this->elementInScope($token['name'])) {
|
||||
$this->generateImpliedEndTags();
|
||||
|
||||
/* Now, if the current node is not an element with
|
||||
the same tag name as that of the token, then this
|
||||
is a parse error. */
|
||||
// XERROR: implement parse error logic
|
||||
|
||||
/* If the stack of open elements has an element in
|
||||
scope with the same tag name as that of the token,
|
||||
then pop elements from this stack until an element
|
||||
with that tag name has been popped from the stack. */
|
||||
do {
|
||||
$node = array_pop($this->stack);
|
||||
} while ($node->tagName !== $token['name']);
|
||||
} else {
|
||||
// parse error
|
||||
}
|
||||
break;
|
||||
|
||||
/* An end tag whose tag name is "form" */
|
||||
case 'form':
|
||||
/* Let node be the element that the form element pointer is set to. */
|
||||
$node = $this->form_pointer;
|
||||
/* Set the form element pointer to null. */
|
||||
$this->form_pointer = null;
|
||||
/* If node is null or the stack of open elements does not
|
||||
* have node in scope, then this is a parse error; ignore the token. */
|
||||
if ($node === null || !in_array($node, $this->stack)) {
|
||||
// parse error
|
||||
$this->ignored = true;
|
||||
} else {
|
||||
/* 1. Generate implied end tags. */
|
||||
$this->generateImpliedEndTags();
|
||||
/* 2. If the current node is not node, then this is a parse error. */
|
||||
if (end($this->stack) !== $node) {
|
||||
// parse error
|
||||
}
|
||||
/* 3. Remove node from the stack of open elements. */
|
||||
array_splice($this->stack, array_search($node, $this->stack, true), 1);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
/* An end tag whose tag name is "p" */
|
||||
case 'p':
|
||||
/* If the stack of open elements has a p element in scope,
|
||||
then generate implied end tags, except for p elements. */
|
||||
if($this->elementInScope('p')) {
|
||||
/* Generate implied end tags, except for elements with
|
||||
* the same tag name as the token. */
|
||||
$this->generateImpliedEndTags(array('p'));
|
||||
|
||||
/* If the current node is not a p element, then this is
|
||||
a parse error. */
|
||||
// XERROR: implement
|
||||
|
||||
/* Pop elements from the stack of open elements until
|
||||
* an element with the same tag name as the token has
|
||||
* been popped from the stack. */
|
||||
do {
|
||||
$node = array_pop($this->stack);
|
||||
} while ($node->tagName !== 'p');
|
||||
|
||||
} else {
|
||||
// parse error
|
||||
$this->emitToken(array(
|
||||
'name' => 'p',
|
||||
'type' => HTML5_Tokenizer::STARTTAG,
|
||||
));
|
||||
$this->emitToken($token);
|
||||
}
|
||||
break;
|
||||
|
||||
/* An end tag whose tag name is "li" */
|
||||
case 'li':
|
||||
/* If the stack of open elements does not have an element
|
||||
* in list item scope with the same tag name as that of the
|
||||
* token, then this is a parse error; ignore the token. */
|
||||
if ($this->elementInScope($token['name'], self::SCOPE_LISTITEM)) {
|
||||
/* Generate implied end tags, except for elements with the
|
||||
* same tag name as the token. */
|
||||
$this->generateImpliedEndTags(array($token['name']));
|
||||
/* If the current node is not an element with the same tag
|
||||
* name as that of the token, then this is a parse error. */
|
||||
// XERROR: parse error
|
||||
/* Pop elements from the stack of open elements until an
|
||||
* element with the same tag name as the token has been
|
||||
* popped from the stack. */
|
||||
do {
|
||||
$node = array_pop($this->stack);
|
||||
} while ($node->tagName !== $token['name']);
|
||||
} else {
|
||||
// XERROR: parse error
|
||||
}
|
||||
break;
|
||||
|
||||
/* An end tag whose tag name is "dc", "dd", "ds", "dt" */
|
||||
case 'dc': case 'dd': case 'ds': case 'dt':
|
||||
if($this->elementInScope($token['name'])) {
|
||||
$this->generateImpliedEndTags(array($token['name']));
|
||||
|
||||
/* If the current node is not an element with the same
|
||||
tag name as the token, then this is a parse error. */
|
||||
// XERROR: implement parse error
|
||||
|
||||
/* Pop elements from the stack of open elements until
|
||||
* an element with the same tag name as the token has
|
||||
* been popped from the stack. */
|
||||
do {
|
||||
$node = array_pop($this->stack);
|
||||
} while ($node->tagName !== $token['name']);
|
||||
|
||||
} else {
|
||||
// XERROR: parse error
|
||||
}
|
||||
break;
|
||||
|
||||
/* An end tag whose tag name is one of: "h1", "h2", "h3", "h4",
|
||||
"h5", "h6" */
|
||||
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
|
||||
$elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
|
||||
|
||||
/* If the stack of open elements has in scope an element whose
|
||||
tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then
|
||||
generate implied end tags. */
|
||||
if($this->elementInScope($elements)) {
|
||||
$this->generateImpliedEndTags();
|
||||
|
||||
/* Now, if the current node is not an element with the same
|
||||
tag name as that of the token, then this is a parse error. */
|
||||
// XERROR: implement parse error
|
||||
|
||||
/* If the stack of open elements has in scope an element
|
||||
whose tag name is one of "h1", "h2", "h3", "h4", "h5", or
|
||||
"h6", then pop elements from the stack until an element
|
||||
with one of those tag names has been popped from the stack. */
|
||||
do {
|
||||
$node = array_pop($this->stack);
|
||||
} while (!in_array($node->tagName, $elements));
|
||||
} else {
|
||||
// parse error
|
||||
}
|
||||
break;
|
||||
|
||||
/* An end tag whose tag name is one of: "a", "b", "big", "em",
|
||||
"font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */
|
||||
case 'a': case 'b': case 'big': case 'code': case 'em': case 'font':
|
||||
case 'i': case 'nobr': case 's': case 'small': case 'strike':
|
||||
case 'strong': case 'tt': case 'u':
|
||||
// XERROR: generally speaking this needs parse error logic
|
||||
/* 1. Let the formatting element be the last element in
|
||||
the list of active formatting elements that:
|
||||
* is between the end of the list and the last scope
|
||||
marker in the list, if any, or the start of the list
|
||||
otherwise, and
|
||||
* has the same tag name as the token.
|
||||
*/
|
||||
while(true) {
|
||||
for($a = count($this->a_formatting) - 1; $a >= 0; $a--) {
|
||||
if($this->a_formatting[$a] === self::MARKER) {
|
||||
break;
|
||||
|
||||
} elseif($this->a_formatting[$a]->tagName === $token['name']) {
|
||||
$formatting_element = $this->a_formatting[$a];
|
||||
$in_stack = in_array($formatting_element, $this->stack, true);
|
||||
$fe_af_pos = $a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* If there is no such node, or, if that node is
|
||||
also in the stack of open elements but the element
|
||||
is not in scope, then this is a parse error. Abort
|
||||
these steps. The token is ignored. */
|
||||
if(!isset($formatting_element) || ($in_stack &&
|
||||
!$this->elementInScope($token['name']))) {
|
||||
$this->ignored = true;
|
||||
break;
|
||||
|
||||
/* Otherwise, if there is such a node, but that node
|
||||
is not in the stack of open elements, then this is a
|
||||
parse error; remove the element from the list, and
|
||||
abort these steps. */
|
||||
} elseif(isset($formatting_element) && !$in_stack) {
|
||||
unset($this->a_formatting[$fe_af_pos]);
|
||||
$this->a_formatting = array_merge($this->a_formatting);
|
||||
break;
|
||||
}
|
||||
|
||||
/* Otherwise, there is a formatting element and that
|
||||
* element is in the stack and is in scope. If the
|
||||
* element is not the current node, this is a parse
|
||||
* error. In any case, proceed with the algorithm as
|
||||
* written in the following steps. */
|
||||
// XERROR: implement me
|
||||
|
||||
/* 2. Let the furthest block be the topmost node in the
|
||||
stack of open elements that is lower in the stack
|
||||
than the formatting element, and is not an element in
|
||||
the phrasing or formatting categories. There might
|
||||
not be one. */
|
||||
$fe_s_pos = array_search($formatting_element, $this->stack, true);
|
||||
$length = count($this->stack);
|
||||
|
||||
for($s = $fe_s_pos + 1; $s < $length; $s++) {
|
||||
$category = $this->getElementCategory($this->stack[$s]);
|
||||
|
||||
if($category !== self::PHRASING && $category !== self::FORMATTING) {
|
||||
$furthest_block = $this->stack[$s];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* 3. If there is no furthest block, then the UA must
|
||||
skip the subsequent steps and instead just pop all
|
||||
the nodes from the bottom of the stack of open
|
||||
elements, from the current node up to the formatting
|
||||
element, and remove the formatting element from the
|
||||
list of active formatting elements. */
|
||||
if(!isset($furthest_block)) {
|
||||
for($n = $length - 1; $n >= $fe_s_pos; $n--) {
|
||||
array_pop($this->stack);
|
||||
}
|
||||
|
||||
unset($this->a_formatting[$fe_af_pos]);
|
||||
$this->a_formatting = array_merge($this->a_formatting);
|
||||
break;
|
||||
}
|
||||
|
||||
/* 4. Let the common ancestor be the element
|
||||
immediately above the formatting element in the stack
|
||||
of open elements. */
|
||||
$common_ancestor = $this->stack[$fe_s_pos - 1];
|
||||
|
||||
/* 5. Let a bookmark note the position of the
|
||||
formatting element in the list of active formatting
|
||||
elements relative to the elements on either side
|
||||
of it in the list. */
|
||||
$bookmark = $fe_af_pos;
|
||||
|
||||
/* 6. Let node and last node be the furthest block.
|
||||
Follow these steps: */
|
||||
$node = $furthest_block;
|
||||
$last_node = $furthest_block;
|
||||
|
||||
while(true) {
|
||||
for($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) {
|
||||
/* 6.1 Let node be the element immediately
|
||||
prior to node in the stack of open elements. */
|
||||
$node = $this->stack[$n];
|
||||
|
||||
/* 6.2 If node is not in the list of active
|
||||
formatting elements, then remove node from
|
||||
the stack of open elements and then go back
|
||||
to step 1. */
|
||||
if(!in_array($node, $this->a_formatting, true)) {
|
||||
array_splice($this->stack, $n, 1);
|
||||
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* 6.3 Otherwise, if node is the formatting
|
||||
element, then go to the next step in the overall
|
||||
algorithm. */
|
||||
if($node === $formatting_element) {
|
||||
break;
|
||||
|
||||
/* 6.4 Otherwise, if last node is the furthest
|
||||
block, then move the aforementioned bookmark to
|
||||
be immediately after the node in the list of
|
||||
active formatting elements. */
|
||||
} elseif($last_node === $furthest_block) {
|
||||
$bookmark = array_search($node, $this->a_formatting, true) + 1;
|
||||
}
|
||||
|
||||
/* 6.5 Create an element for the token for which
|
||||
* the element node was created, replace the entry
|
||||
* for node in the list of active formatting
|
||||
* elements with an entry for the new element,
|
||||
* replace the entry for node in the stack of open
|
||||
* elements with an entry for the new element, and
|
||||
* let node be the new element. */
|
||||
// we don't know what the token is anymore
|
||||
// XDOM
|
||||
$clone = $node->cloneNode();
|
||||
$a_pos = array_search($node, $this->a_formatting, true);
|
||||
$s_pos = array_search($node, $this->stack, true);
|
||||
$this->a_formatting[$a_pos] = $clone;
|
||||
$this->stack[$s_pos] = $clone;
|
||||
$node = $clone;
|
||||
|
||||
/* 6.6 Insert last node into node, first removing
|
||||
it from its previous parent node if any. */
|
||||
// XDOM
|
||||
if($last_node->parentNode !== null) {
|
||||
$last_node->parentNode->removeChild($last_node);
|
||||
}
|
||||
|
||||
// XDOM
|
||||
$node->appendChild($last_node);
|
||||
|
||||
/* 6.7 Let last node be node. */
|
||||
$last_node = $node;
|
||||
|
||||
/* 6.8 Return to step 1 of this inner set of steps. */
|
||||
}
|
||||
|
||||
/* 7. If the common ancestor node is a table, tbody,
|
||||
* tfoot, thead, or tr element, then, foster parent
|
||||
* whatever last node ended up being in the previous
|
||||
* step, first removing it from its previous parent
|
||||
* node if any. */
|
||||
// XDOM
|
||||
if ($last_node->parentNode) { // common step
|
||||
$last_node->parentNode->removeChild($last_node);
|
||||
}
|
||||
if (in_array($common_ancestor->tagName, array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
|
||||
$this->fosterParent($last_node);
|
||||
/* Otherwise, append whatever last node ended up being
|
||||
* in the previous step to the common ancestor node,
|
||||
* first removing it from its previous parent node if
|
||||
* any. */
|
||||
} else {
|
||||
// XDOM
|
||||
$common_ancestor->appendChild($last_node);
|
||||
}
|
||||
|
||||
/* 8. Create an element for the token for which the
|
||||
* formatting element was created. */
|
||||
// XDOM
|
||||
$clone = $formatting_element->cloneNode();
|
||||
|
||||
/* 9. Take all of the child nodes of the furthest
|
||||
block and append them to the element created in the
|
||||
last step. */
|
||||
// XDOM
|
||||
while($furthest_block->hasChildNodes()) {
|
||||
$child = $furthest_block->firstChild;
|
||||
$furthest_block->removeChild($child);
|
||||
$clone->appendChild($child);
|
||||
}
|
||||
|
||||
/* 10. Append that clone to the furthest block. */
|
||||
// XDOM
|
||||
$furthest_block->appendChild($clone);
|
||||
|
||||
/* 11. Remove the formatting element from the list
|
||||
of active formatting elements, and insert the new element
|
||||
into the list of active formatting elements at the
|
||||
position of the aforementioned bookmark. */
|
||||
$fe_af_pos = array_search($formatting_element, $this->a_formatting, true);
|
||||
array_splice($this->a_formatting, $fe_af_pos, 1);
|
||||
|
||||
$af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1);
|
||||
$af_part2 = array_slice($this->a_formatting, $bookmark);
|
||||
$this->a_formatting = array_merge($af_part1, array($clone), $af_part2);
|
||||
|
||||
/* 12. Remove the formatting element from the stack
|
||||
of open elements, and insert the new element into the stack
|
||||
of open elements immediately below the position of the
|
||||
furthest block in that stack. */
|
||||
$fe_s_pos = array_search($formatting_element, $this->stack, true);
|
||||
array_splice($this->stack, $fe_s_pos, 1);
|
||||
|
||||
$fb_s_pos = array_search($furthest_block, $this->stack, true);
|
||||
$s_part1 = array_slice($this->stack, 0, $fb_s_pos + 1);
|
||||
$s_part2 = array_slice($this->stack, $fb_s_pos + 1);
|
||||
$this->stack = array_merge($s_part1, array($clone), $s_part2);
|
||||
|
||||
/* 13. Jump back to step 1 in this series of steps. */
|
||||
unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'applet': case 'button': case 'marquee': case 'object':
|
||||
/* If the stack of open elements has an element in scope whose
|
||||
tag name matches the tag name of the token, then generate implied
|
||||
tags. */
|
||||
if($this->elementInScope($token['name'])) {
|
||||
$this->generateImpliedEndTags();
|
||||
|
||||
/* Now, if the current node is not an element with the same
|
||||
tag name as the token, then this is a parse error. */
|
||||
// XERROR: implement logic
|
||||
|
||||
/* Pop elements from the stack of open elements until
|
||||
* an element with the same tag name as the token has
|
||||
* been popped from the stack. */
|
||||
do {
|
||||
$node = array_pop($this->stack);
|
||||
} while ($node->tagName !== $token['name']);
|
||||
|
||||
/* Clear the list of active formatting elements up to the
|
||||
* last marker. */
|
||||
$keys = array_keys($this->a_formatting, self::MARKER, true);
|
||||
$marker = end($keys);
|
||||
|
||||
for($n = count($this->a_formatting) - 1; $n > $marker; $n--) {
|
||||
array_pop($this->a_formatting);
|
||||
}
|
||||
} else {
|
||||
// parse error
|
||||
}
|
||||
break;
|
||||
|
||||
case 'br':
|
||||
// Parse error
|
||||
$this->emitToken(array(
|
||||
'name' => 'br',
|
||||
'type' => HTML5_Tokenizer::STARTTAG,
|
||||
));
|
||||
break;
|
||||
|
||||
/* An end tag token not covered by the previous entries */
|
||||
default:
|
||||
for($n = count($this->stack) - 1; $n >= 0; $n--) {
|
||||
/* Initialise node to be the current node (the bottommost
|
||||
node of the stack). */
|
||||
$node = $this->stack[$n];
|
||||
|
||||
/* If node has the same tag name as the end tag token,
|
||||
then: */
|
||||
if($token['name'] === $node->tagName) {
|
||||
/* Generate implied end tags. */
|
||||
$this->generateImpliedEndTags();
|
||||
|
||||
/* If the tag name of the end tag token does not
|
||||
match the tag name of the current node, this is a
|
||||
parse error. */
|
||||
// XERROR: implement this
|
||||
|
||||
/* Pop all the nodes from the current node up to
|
||||
node, including node, then stop these steps. */
|
||||
// XSKETCHY
|
||||
do {
|
||||
$pop = array_pop($this->stack);
|
||||
} while ($pop !== $node);
|
||||
break;
|
||||
|
||||
} else {
|
||||
$category = $this->getElementCategory($node);
|
||||
|
||||
if($category !== self::FORMATTING && $category !== self::PHRASING) {
|
||||
/* Otherwise, if node is in neither the formatting
|
||||
category nor the phrasing category, then this is a
|
||||
parse error. Stop this algorithm. The end tag token
|
||||
is ignored. */
|
||||
$this->ignored = true;
|
||||
break;
|
||||
// parse error
|
||||
}
|
||||
}
|
||||
/* Set node to the previous entry in the stack of open elements. Loop. */
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_CDATA_RCDATA:
|
||||
if (
|
||||
$token['type'] === HTML5_Tokenizer::CHARACTER ||
|
||||
$token['type'] === HTML5_Tokenizer::SPACECHARACTER
|
||||
) {
|
||||
$this->insertText($token['data']);
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::EOF) {
|
||||
// parse error
|
||||
/* If the current node is a script element, mark the script
|
||||
* element as "already executed". */
|
||||
// probably not necessary
|
||||
array_pop($this->stack);
|
||||
$this->mode = $this->original_mode;
|
||||
$this->emitToken($token);
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'script') {
|
||||
array_pop($this->stack);
|
||||
$this->mode = $this->original_mode;
|
||||
// we're ignoring all of the execution stuff
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::ENDTAG) {
|
||||
array_pop($this->stack);
|
||||
$this->mode = $this->original_mode;
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_TABLE:
|
||||
$clear = array('html', 'table');
|
||||
|
||||
/* A character token */
|
||||
if ($token['type'] === HTML5_Tokenizer::CHARACTER ||
|
||||
$token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
|
||||
/* Let the pending table character tokens
|
||||
* be an empty list of tokens. */
|
||||
$this->pendingTableCharacters = "";
|
||||
$this->pendingTableCharactersDirty = false;
|
||||
/* Let the original insertion mode be the current
|
||||
* insertion mode. */
|
||||
$this->original_mode = $this->mode;
|
||||
/* Switch the insertion mode to
|
||||
* "in table text" and
|
||||
* reprocess the token. */
|
||||
$this->mode = self::IN_TABLE_TEXT;
|
||||
$this->emitToken($token);
|
||||
|
||||
/* A comment token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
/* Append a Comment node to the current node with the data
|
||||
attribute set to the data given in the comment token. */
|
||||
$this->insertComment($token['data']);
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
// parse error
|
||||
|
||||
/* A start tag whose tag name is "caption" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
$token['name'] === 'caption') {
|
||||
/* Clear the stack back to a table context. */
|
||||
$this->clearStackToTableContext($clear);
|
||||
|
||||
/* Insert a marker at the end of the list of active
|
||||
formatting elements. */
|
||||
$this->a_formatting[] = self::MARKER;
|
||||
|
||||
/* Insert an HTML element for the token, then switch the
|
||||
insertion mode to "in caption". */
|
||||
$this->insertElement($token);
|
||||
$this->mode = self::IN_CAPTION;
|
||||
|
||||
/* A start tag whose tag name is "colgroup" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
$token['name'] === 'colgroup') {
|
||||
/* Clear the stack back to a table context. */
|
||||
$this->clearStackToTableContext($clear);
|
||||
|
||||
/* Insert an HTML element for the token, then switch the
|
||||
insertion mode to "in column group". */
|
||||
$this->insertElement($token);
|
||||
$this->mode = self::IN_COLUMN_GROUP;
|
||||
|
||||
/* A start tag whose tag name is "col" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
$token['name'] === 'col') {
|
||||
$this->emitToken(array(
|
||||
'name' => 'colgroup',
|
||||
'type' => HTML5_Tokenizer::STARTTAG,
|
||||
'attr' => array()
|
||||
));
|
||||
|
||||
$this->emitToken($token);
|
||||
|
||||
/* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
|
||||
array('tbody', 'tfoot', 'thead'))) {
|
||||
/* Clear the stack back to a table context. */
|
||||
$this->clearStackToTableContext($clear);
|
||||
|
||||
/* Insert an HTML element for the token, then switch the insertion
|
||||
mode to "in table body". */
|
||||
$this->insertElement($token);
|
||||
$this->mode = self::IN_TABLE_BODY;
|
||||
|
||||
/* A start tag whose tag name is one of: "td", "th", "tr" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
in_array($token['name'], array('td', 'th', 'tr'))) {
|
||||
/* Act as if a start tag token with the tag name "tbody" had been
|
||||
seen, then reprocess the current token. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'tbody',
|
||||
'type' => HTML5_Tokenizer::STARTTAG,
|
||||
'attr' => array()
|
||||
));
|
||||
|
||||
$this->emitToken($token);
|
||||
|
||||
/* A start tag whose tag name is "table" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
$token['name'] === 'table') {
|
||||
/* Parse error. Act as if an end tag token with the tag name "table"
|
||||
had been seen, then, if that token wasn't ignored, reprocess the
|
||||
current token. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'table',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
|
||||
if (!$this->ignored) $this->emitToken($token);
|
||||
|
||||
/* An end tag whose tag name is "table" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
$token['name'] === 'table') {
|
||||
/* If the stack of open elements does not have an element in table
|
||||
scope with the same tag name as the token, this is a parse error.
|
||||
Ignore the token. (fragment case) */
|
||||
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
|
||||
$this->ignored = true;
|
||||
|
||||
/* Otherwise: */
|
||||
} else {
|
||||
do {
|
||||
$node = array_pop($this->stack);
|
||||
} while ($node->tagName !== 'table');
|
||||
|
||||
/* Reset the insertion mode appropriately. */
|
||||
$this->resetInsertionMode();
|
||||
}
|
||||
|
||||
/* An end tag whose tag name is one of: "body", "caption", "col",
|
||||
"colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
|
||||
array('body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td',
|
||||
'tfoot', 'th', 'thead', 'tr'))) {
|
||||
// Parse error. Ignore the token.
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
($token['name'] === 'style' || $token['name'] === 'script')) {
|
||||
$this->processWithRulesFor($token, self::IN_HEAD);
|
||||
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'input' &&
|
||||
// assignment is intentional
|
||||
/* If the token does not have an attribute with the name "type", or
|
||||
* if it does, but that attribute's value is not an ASCII
|
||||
* case-insensitive match for the string "hidden", then: act as
|
||||
* described in the "anything else" entry below. */
|
||||
($type = $this->getAttr($token, 'type')) && strtolower($type) === 'hidden') {
|
||||
// I.e., if its an input with the type attribute == 'hidden'
|
||||
/* Otherwise */
|
||||
// parse error
|
||||
$this->insertElement($token);
|
||||
array_pop($this->stack);
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::EOF) {
|
||||
/* If the current node is not the root html element, then this is a parse error. */
|
||||
if (end($this->stack)->tagName !== 'html') {
|
||||
// Note: It can only be the current node in the fragment case.
|
||||
// parse error
|
||||
}
|
||||
/* Stop parsing. */
|
||||
/* Anything else */
|
||||
} else {
|
||||
/* Parse error. Process the token as if the insertion mode was "in
|
||||
body", with the following exception: */
|
||||
|
||||
$old = $this->foster_parent;
|
||||
$this->foster_parent = true;
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
$this->foster_parent = $old;
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_TABLE_TEXT:
|
||||
/* A character token */
|
||||
if($token['type'] === HTML5_Tokenizer::CHARACTER) {
|
||||
/* Append the character token to the pending table
|
||||
* character tokens list. */
|
||||
$this->pendingTableCharacters .= $token['data'];
|
||||
$this->pendingTableCharactersDirty = true;
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
|
||||
$this->pendingTableCharacters .= $token['data'];
|
||||
/* Anything else */
|
||||
} else {
|
||||
if ($this->pendingTableCharacters !== '' && is_string($this->pendingTableCharacters)) {
|
||||
/* If any of the tokens in the pending table character tokens list
|
||||
* are character tokens that are not one of U+0009 CHARACTER
|
||||
* TABULATION, U+000A LINE FEED (LF), U+000C FORM FEED (FF), or
|
||||
* U+0020 SPACE, then reprocess those character tokens using the
|
||||
* rules given in the "anything else" entry in the in table"
|
||||
* insertion mode.*/
|
||||
if ($this->pendingTableCharactersDirty) {
|
||||
/* Parse error. Process the token using the rules for the
|
||||
* "in body" insertion mode, except that if the current
|
||||
* node is a table, tbody, tfoot, thead, or tr element,
|
||||
* then, whenever a node would be inserted into the current
|
||||
* node, it must instead be foster parented. */
|
||||
// XERROR
|
||||
$old = $this->foster_parent;
|
||||
$this->foster_parent = true;
|
||||
$text_token = array(
|
||||
'type' => HTML5_Tokenizer::CHARACTER,
|
||||
'data' => $this->pendingTableCharacters,
|
||||
);
|
||||
$this->processWithRulesFor($text_token, self::IN_BODY);
|
||||
$this->foster_parent = $old;
|
||||
|
||||
/* Otherwise, insert the characters given by the pending table
|
||||
* character tokens list into the current node. */
|
||||
} else {
|
||||
$this->insertText($this->pendingTableCharacters);
|
||||
}
|
||||
$this->pendingTableCharacters = null;
|
||||
$this->pendingTableCharactersNull = null;
|
||||
}
|
||||
|
||||
/* Switch the insertion mode to the original insertion mode and
|
||||
* reprocess the token.
|
||||
*/
|
||||
$this->mode = $this->original_mode;
|
||||
$this->emitToken($token);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_CAPTION:
|
||||
/* An end tag whose tag name is "caption" */
|
||||
if($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'caption') {
|
||||
/* If the stack of open elements does not have an element in table
|
||||
scope with the same tag name as the token, this is a parse error.
|
||||
Ignore the token. (fragment case) */
|
||||
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
|
||||
$this->ignored = true;
|
||||
// Ignore
|
||||
|
||||
/* Otherwise: */
|
||||
} else {
|
||||
/* Generate implied end tags. */
|
||||
$this->generateImpliedEndTags();
|
||||
|
||||
/* Now, if the current node is not a caption element, then this
|
||||
is a parse error. */
|
||||
// XERROR: implement
|
||||
|
||||
/* Pop elements from this stack until a caption element has
|
||||
been popped from the stack. */
|
||||
do {
|
||||
$node = array_pop($this->stack);
|
||||
} while ($node->tagName !== 'caption');
|
||||
|
||||
/* Clear the list of active formatting elements up to the last
|
||||
marker. */
|
||||
$this->clearTheActiveFormattingElementsUpToTheLastMarker();
|
||||
|
||||
/* Switch the insertion mode to "in table". */
|
||||
$this->mode = self::IN_TABLE;
|
||||
}
|
||||
|
||||
/* A start tag whose tag name is one of: "caption", "col", "colgroup",
|
||||
"tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag
|
||||
name is "table" */
|
||||
} elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
|
||||
array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
|
||||
'thead', 'tr'))) || ($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
$token['name'] === 'table')) {
|
||||
/* Parse error. Act as if an end tag with the tag name "caption"
|
||||
had been seen, then, if that token wasn't ignored, reprocess the
|
||||
current token. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'caption',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
|
||||
if (!$this->ignored) $this->emitToken($token);
|
||||
|
||||
/* An end tag whose tag name is one of: "body", "col", "colgroup",
|
||||
"html", "tbody", "td", "tfoot", "th", "thead", "tr" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
|
||||
array('body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th',
|
||||
'thead', 'tr'))) {
|
||||
// Parse error. Ignore the token.
|
||||
$this->ignored = true;
|
||||
|
||||
/* Anything else */
|
||||
} else {
|
||||
/* Process the token as if the insertion mode was "in body". */
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_COLUMN_GROUP:
|
||||
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
|
||||
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
|
||||
or U+0020 SPACE */
|
||||
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
|
||||
/* Append the character to the current node. */
|
||||
$this->insertText($token['data']);
|
||||
|
||||
/* A comment token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
/* Append a Comment node to the current node with the data
|
||||
attribute set to the data given in the comment token. */
|
||||
$this->insertToken($token['data']);
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
// parse error
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
|
||||
/* A start tag whose tag name is "col" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'col') {
|
||||
/* Insert a col element for the token. Immediately pop the current
|
||||
node off the stack of open elements. */
|
||||
$this->insertElement($token);
|
||||
array_pop($this->stack);
|
||||
// XERROR: Acknowledge the token's self-closing flag, if it is set.
|
||||
|
||||
/* An end tag whose tag name is "colgroup" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
$token['name'] === 'colgroup') {
|
||||
/* If the current node is the root html element, then this is a
|
||||
parse error, ignore the token. (fragment case) */
|
||||
if(end($this->stack)->tagName === 'html') {
|
||||
$this->ignored = true;
|
||||
|
||||
/* Otherwise, pop the current node (which will be a colgroup
|
||||
element) from the stack of open elements. Switch the insertion
|
||||
mode to "in table". */
|
||||
} else {
|
||||
array_pop($this->stack);
|
||||
$this->mode = self::IN_TABLE;
|
||||
}
|
||||
|
||||
/* An end tag whose tag name is "col" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'col') {
|
||||
/* Parse error. Ignore the token. */
|
||||
$this->ignored = true;
|
||||
|
||||
/* An end-of-file token */
|
||||
/* If the current node is the root html element */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::EOF && end($this->stack)->tagName === 'html') {
|
||||
/* Stop parsing */
|
||||
|
||||
/* Anything else */
|
||||
} else {
|
||||
/* Act as if an end tag with the tag name "colgroup" had been seen,
|
||||
and then, if that token wasn't ignored, reprocess the current token. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'colgroup',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
|
||||
if (!$this->ignored) $this->emitToken($token);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_TABLE_BODY:
|
||||
$clear = array('tbody', 'tfoot', 'thead', 'html');
|
||||
|
||||
/* A start tag whose tag name is "tr" */
|
||||
if($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'tr') {
|
||||
/* Clear the stack back to a table body context. */
|
||||
$this->clearStackToTableContext($clear);
|
||||
|
||||
/* Insert a tr element for the token, then switch the insertion
|
||||
mode to "in row". */
|
||||
$this->insertElement($token);
|
||||
$this->mode = self::IN_ROW;
|
||||
|
||||
/* A start tag whose tag name is one of: "th", "td" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
($token['name'] === 'th' || $token['name'] === 'td')) {
|
||||
/* Parse error. Act as if a start tag with the tag name "tr" had
|
||||
been seen, then reprocess the current token. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'tr',
|
||||
'type' => HTML5_Tokenizer::STARTTAG,
|
||||
'attr' => array()
|
||||
));
|
||||
|
||||
$this->emitToken($token);
|
||||
|
||||
/* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
|
||||
/* If the stack of open elements does not have an element in table
|
||||
scope with the same tag name as the token, this is a parse error.
|
||||
Ignore the token. */
|
||||
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
|
||||
// Parse error
|
||||
$this->ignored = true;
|
||||
|
||||
/* Otherwise: */
|
||||
} else {
|
||||
/* Clear the stack back to a table body context. */
|
||||
$this->clearStackToTableContext($clear);
|
||||
|
||||
/* Pop the current node from the stack of open elements. Switch
|
||||
the insertion mode to "in table". */
|
||||
array_pop($this->stack);
|
||||
$this->mode = self::IN_TABLE;
|
||||
}
|
||||
|
||||
/* A start tag whose tag name is one of: "caption", "col", "colgroup",
|
||||
"tbody", "tfoot", "thead", or an end tag whose tag name is "table" */
|
||||
} elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
|
||||
array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead'))) ||
|
||||
($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'table')) {
|
||||
/* If the stack of open elements does not have a tbody, thead, or
|
||||
tfoot element in table scope, this is a parse error. Ignore the
|
||||
token. (fragment case) */
|
||||
if(!$this->elementInScope(array('tbody', 'thead', 'tfoot'), self::SCOPE_TABLE)) {
|
||||
// parse error
|
||||
$this->ignored = true;
|
||||
|
||||
/* Otherwise: */
|
||||
} else {
|
||||
/* Clear the stack back to a table body context. */
|
||||
$this->clearStackToTableContext($clear);
|
||||
|
||||
/* Act as if an end tag with the same tag name as the current
|
||||
node ("tbody", "tfoot", or "thead") had been seen, then
|
||||
reprocess the current token. */
|
||||
$this->emitToken(array(
|
||||
'name' => end($this->stack)->tagName,
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
|
||||
$this->emitToken($token);
|
||||
}
|
||||
|
||||
/* An end tag whose tag name is one of: "body", "caption", "col",
|
||||
"colgroup", "html", "td", "th", "tr" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
|
||||
array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) {
|
||||
/* Parse error. Ignore the token. */
|
||||
$this->ignored = true;
|
||||
|
||||
/* Anything else */
|
||||
} else {
|
||||
/* Process the token as if the insertion mode was "in table". */
|
||||
$this->processWithRulesFor($token, self::IN_TABLE);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_ROW:
|
||||
$clear = array('tr', 'html');
|
||||
|
||||
/* A start tag whose tag name is one of: "th", "td" */
|
||||
if($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
($token['name'] === 'th' || $token['name'] === 'td')) {
|
||||
/* Clear the stack back to a table row context. */
|
||||
$this->clearStackToTableContext($clear);
|
||||
|
||||
/* Insert an HTML element for the token, then switch the insertion
|
||||
mode to "in cell". */
|
||||
$this->insertElement($token);
|
||||
$this->mode = self::IN_CELL;
|
||||
|
||||
/* Insert a marker at the end of the list of active formatting
|
||||
elements. */
|
||||
$this->a_formatting[] = self::MARKER;
|
||||
|
||||
/* An end tag whose tag name is "tr" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'tr') {
|
||||
/* If the stack of open elements does not have an element in table
|
||||
scope with the same tag name as the token, this is a parse error.
|
||||
Ignore the token. (fragment case) */
|
||||
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
|
||||
// Ignore.
|
||||
$this->ignored = true;
|
||||
|
||||
/* Otherwise: */
|
||||
} else {
|
||||
/* Clear the stack back to a table row context. */
|
||||
$this->clearStackToTableContext($clear);
|
||||
|
||||
/* Pop the current node (which will be a tr element) from the
|
||||
stack of open elements. Switch the insertion mode to "in table
|
||||
body". */
|
||||
array_pop($this->stack);
|
||||
$this->mode = self::IN_TABLE_BODY;
|
||||
}
|
||||
|
||||
/* A start tag whose tag name is one of: "caption", "col", "colgroup",
|
||||
"tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */
|
||||
} elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
|
||||
array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr'))) ||
|
||||
($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'table')) {
|
||||
/* Act as if an end tag with the tag name "tr" had been seen, then,
|
||||
if that token wasn't ignored, reprocess the current token. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'tr',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
if (!$this->ignored) $this->emitToken($token);
|
||||
|
||||
/* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
|
||||
/* If the stack of open elements does not have an element in table
|
||||
scope with the same tag name as the token, this is a parse error.
|
||||
Ignore the token. */
|
||||
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
|
||||
$this->ignored = true;
|
||||
|
||||
/* Otherwise: */
|
||||
} else {
|
||||
/* Otherwise, act as if an end tag with the tag name "tr" had
|
||||
been seen, then reprocess the current token. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'tr',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
|
||||
$this->emitToken($token);
|
||||
}
|
||||
|
||||
/* An end tag whose tag name is one of: "body", "caption", "col",
|
||||
"colgroup", "html", "td", "th" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
|
||||
array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th'))) {
|
||||
/* Parse error. Ignore the token. */
|
||||
$this->ignored = true;
|
||||
|
||||
/* Anything else */
|
||||
} else {
|
||||
/* Process the token as if the insertion mode was "in table". */
|
||||
$this->processWithRulesFor($token, self::IN_TABLE);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_CELL:
|
||||
/* An end tag whose tag name is one of: "td", "th" */
|
||||
if($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
($token['name'] === 'td' || $token['name'] === 'th')) {
|
||||
/* If the stack of open elements does not have an element in table
|
||||
scope with the same tag name as that of the token, then this is a
|
||||
parse error and the token must be ignored. */
|
||||
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
|
||||
$this->ignored = true;
|
||||
|
||||
/* Otherwise: */
|
||||
} else {
|
||||
/* Generate implied end tags, except for elements with the same
|
||||
tag name as the token. */
|
||||
$this->generateImpliedEndTags(array($token['name']));
|
||||
|
||||
/* Now, if the current node is not an element with the same tag
|
||||
name as the token, then this is a parse error. */
|
||||
// XERROR: Implement parse error code
|
||||
|
||||
/* Pop elements from this stack until an element with the same
|
||||
tag name as the token has been popped from the stack. */
|
||||
do {
|
||||
$node = array_pop($this->stack);
|
||||
} while ($node->tagName !== $token['name']);
|
||||
|
||||
/* Clear the list of active formatting elements up to the last
|
||||
marker. */
|
||||
$this->clearTheActiveFormattingElementsUpToTheLastMarker();
|
||||
|
||||
/* Switch the insertion mode to "in row". (The current node
|
||||
will be a tr element at this point.) */
|
||||
$this->mode = self::IN_ROW;
|
||||
}
|
||||
|
||||
/* A start tag whose tag name is one of: "caption", "col", "colgroup",
|
||||
"tbody", "td", "tfoot", "th", "thead", "tr" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
|
||||
array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
|
||||
'thead', 'tr'))) {
|
||||
/* If the stack of open elements does not have a td or th element
|
||||
in table scope, then this is a parse error; ignore the token.
|
||||
(fragment case) */
|
||||
if(!$this->elementInScope(array('td', 'th'), self::SCOPE_TABLE)) {
|
||||
// parse error
|
||||
$this->ignored = true;
|
||||
|
||||
/* Otherwise, close the cell (see below) and reprocess the current
|
||||
token. */
|
||||
} else {
|
||||
$this->closeCell();
|
||||
$this->emitToken($token);
|
||||
}
|
||||
|
||||
/* An end tag whose tag name is one of: "body", "caption", "col",
|
||||
"colgroup", "html" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
|
||||
array('body', 'caption', 'col', 'colgroup', 'html'))) {
|
||||
/* Parse error. Ignore the token. */
|
||||
$this->ignored = true;
|
||||
|
||||
/* An end tag whose tag name is one of: "table", "tbody", "tfoot",
|
||||
"thead", "tr" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
|
||||
array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
|
||||
/* If the stack of open elements does not have a td or th element
|
||||
in table scope, then this is a parse error; ignore the token.
|
||||
(innerHTML case) */
|
||||
if(!$this->elementInScope(array('td', 'th'), self::SCOPE_TABLE)) {
|
||||
// Parse error
|
||||
$this->ignored = true;
|
||||
|
||||
/* Otherwise, close the cell (see below) and reprocess the current
|
||||
token. */
|
||||
} else {
|
||||
$this->closeCell();
|
||||
$this->emitToken($token);
|
||||
}
|
||||
|
||||
/* Anything else */
|
||||
} else {
|
||||
/* Process the token as if the insertion mode was "in body". */
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_SELECT:
|
||||
/* Handle the token as follows: */
|
||||
|
||||
/* A character token */
|
||||
if(
|
||||
$token['type'] === HTML5_Tokenizer::CHARACTER ||
|
||||
$token['type'] === HTML5_Tokenizer::SPACECHARACTER
|
||||
) {
|
||||
/* Append the token's character to the current node. */
|
||||
$this->insertText($token['data']);
|
||||
|
||||
/* A comment token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
/* Append a Comment node to the current node with the data
|
||||
attribute set to the data given in the comment token. */
|
||||
$this->insertComment($token['data']);
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
// parse error
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
|
||||
$this->processWithRulesFor($token, self::INBODY);
|
||||
|
||||
/* A start tag token whose tag name is "option" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
$token['name'] === 'option') {
|
||||
/* If the current node is an option element, act as if an end tag
|
||||
with the tag name "option" had been seen. */
|
||||
if(end($this->stack)->tagName === 'option') {
|
||||
$this->emitToken(array(
|
||||
'name' => 'option',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
|
||||
/* A start tag token whose tag name is "optgroup" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
$token['name'] === 'optgroup') {
|
||||
/* If the current node is an option element, act as if an end tag
|
||||
with the tag name "option" had been seen. */
|
||||
if(end($this->stack)->tagName === 'option') {
|
||||
$this->emitToken(array(
|
||||
'name' => 'option',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* If the current node is an optgroup element, act as if an end tag
|
||||
with the tag name "optgroup" had been seen. */
|
||||
if(end($this->stack)->tagName === 'optgroup') {
|
||||
$this->emitToken(array(
|
||||
'name' => 'optgroup',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
|
||||
/* An end tag token whose tag name is "optgroup" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
$token['name'] === 'optgroup') {
|
||||
/* First, if the current node is an option element, and the node
|
||||
immediately before it in the stack of open elements is an optgroup
|
||||
element, then act as if an end tag with the tag name "option" had
|
||||
been seen. */
|
||||
$elements_in_stack = count($this->stack);
|
||||
|
||||
if($this->stack[$elements_in_stack - 1]->tagName === 'option' &&
|
||||
$this->stack[$elements_in_stack - 2]->tagName === 'optgroup') {
|
||||
$this->emitToken(array(
|
||||
'name' => 'option',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
}
|
||||
|
||||
/* If the current node is an optgroup element, then pop that node
|
||||
from the stack of open elements. Otherwise, this is a parse error,
|
||||
ignore the token. */
|
||||
if(end($this->stack)->tagName === 'optgroup') {
|
||||
array_pop($this->stack);
|
||||
} else {
|
||||
// parse error
|
||||
$this->ignored = true;
|
||||
}
|
||||
|
||||
/* An end tag token whose tag name is "option" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
$token['name'] === 'option') {
|
||||
/* If the current node is an option element, then pop that node
|
||||
from the stack of open elements. Otherwise, this is a parse error,
|
||||
ignore the token. */
|
||||
if(end($this->stack)->tagName === 'option') {
|
||||
array_pop($this->stack);
|
||||
} else {
|
||||
// parse error
|
||||
$this->ignored = true;
|
||||
}
|
||||
|
||||
/* An end tag whose tag name is "select" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
$token['name'] === 'select') {
|
||||
/* If the stack of open elements does not have an element in table
|
||||
scope with the same tag name as the token, this is a parse error.
|
||||
Ignore the token. (fragment case) */
|
||||
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
|
||||
$this->ignored = true;
|
||||
// parse error
|
||||
|
||||
/* Otherwise: */
|
||||
} else {
|
||||
/* Pop elements from the stack of open elements until a select
|
||||
element has been popped from the stack. */
|
||||
do {
|
||||
$node = array_pop($this->stack);
|
||||
} while ($node->tagName !== 'select');
|
||||
|
||||
/* Reset the insertion mode appropriately. */
|
||||
$this->resetInsertionMode();
|
||||
}
|
||||
|
||||
/* A start tag whose tag name is "select" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'select') {
|
||||
/* Parse error. Act as if the token had been an end tag with the
|
||||
tag name "select" instead. */
|
||||
$this->emitToken(array(
|
||||
'name' => 'select',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
($token['name'] === 'input' || $token['name'] === 'keygen' || $token['name'] === 'textarea')) {
|
||||
// parse error
|
||||
$this->emitToken(array(
|
||||
'name' => 'select',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
$this->emitToken($token);
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'script') {
|
||||
$this->processWithRulesFor($token, self::IN_HEAD);
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::EOF) {
|
||||
// XERROR: If the current node is not the root html element, then this is a parse error.
|
||||
/* Stop parsing */
|
||||
|
||||
/* Anything else */
|
||||
} else {
|
||||
/* Parse error. Ignore the token. */
|
||||
$this->ignored = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_SELECT_IN_TABLE:
|
||||
|
||||
if($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
in_array($token['name'], array('caption', 'table', 'tbody',
|
||||
'tfoot', 'thead', 'tr', 'td', 'th'))) {
|
||||
// parse error
|
||||
$this->emitToken(array(
|
||||
'name' => 'select',
|
||||
'type' => HTML5_Tokenizer::ENDTAG,
|
||||
));
|
||||
$this->emitToken($token);
|
||||
|
||||
/* An end tag whose tag name is one of: "caption", "table", "tbody",
|
||||
"tfoot", "thead", "tr", "td", "th" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
in_array($token['name'], array('caption', 'table', 'tbody', 'tfoot', 'thead', 'tr', 'td', 'th'))) {
|
||||
/* Parse error. */
|
||||
// parse error
|
||||
|
||||
/* If the stack of open elements has an element in table scope with
|
||||
the same tag name as that of the token, then act as if an end tag
|
||||
with the tag name "select" had been seen, and reprocess the token.
|
||||
Otherwise, ignore the token. */
|
||||
if($this->elementInScope($token['name'], self::SCOPE_TABLE)) {
|
||||
$this->emitToken(array(
|
||||
'name' => 'select',
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
|
||||
$this->emitToken($token);
|
||||
} else {
|
||||
$this->ignored = true;
|
||||
}
|
||||
} else {
|
||||
$this->processWithRulesFor($token, self::IN_SELECT);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_FOREIGN_CONTENT:
|
||||
if ($token['type'] === HTML5_Tokenizer::CHARACTER ||
|
||||
$token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
|
||||
$this->insertText($token['data']);
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
$this->insertComment($token['data']);
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
// XERROR: parse error
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
$token['name'] === 'script' && end($this->stack)->tagName === 'script' &&
|
||||
// XDOM
|
||||
end($this->stack)->namespaceURI === self::NS_SVG) {
|
||||
array_pop($this->stack);
|
||||
// a bunch of script running mumbo jumbo
|
||||
} elseif (
|
||||
($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
((
|
||||
$token['name'] !== 'mglyph' &&
|
||||
$token['name'] !== 'malignmark' &&
|
||||
// XDOM
|
||||
end($this->stack)->namespaceURI === self::NS_MATHML &&
|
||||
in_array(end($this->stack)->tagName, array('mi', 'mo', 'mn', 'ms', 'mtext'))
|
||||
) ||
|
||||
(
|
||||
$token['name'] === 'svg' &&
|
||||
// XDOM
|
||||
end($this->stack)->namespaceURI === self::NS_MATHML &&
|
||||
end($this->stack)->tagName === 'annotation-xml'
|
||||
) ||
|
||||
(
|
||||
// XDOM
|
||||
end($this->stack)->namespaceURI === self::NS_SVG &&
|
||||
in_array(end($this->stack)->tagName, array('foreignObject', 'desc', 'title'))
|
||||
) ||
|
||||
(
|
||||
// XSKETCHY && XDOM
|
||||
end($this->stack)->namespaceURI === self::NS_HTML
|
||||
))
|
||||
) || $token['type'] === HTML5_Tokenizer::ENDTAG
|
||||
) {
|
||||
$this->processWithRulesFor($token, $this->secondary_mode);
|
||||
/* If, after doing so, the insertion mode is still "in foreign
|
||||
* content", but there is no element in scope that has a namespace
|
||||
* other than the HTML namespace, switch the insertion mode to the
|
||||
* secondary insertion mode. */
|
||||
if ($this->mode === self::IN_FOREIGN_CONTENT) {
|
||||
$found = false;
|
||||
// this basically duplicates elementInScope()
|
||||
for ($i = count($this->stack) - 1; $i >= 0; $i--) {
|
||||
// XDOM
|
||||
$node = $this->stack[$i];
|
||||
if ($node->namespaceURI !== self::NS_HTML) {
|
||||
$found = true;
|
||||
break;
|
||||
} elseif (in_array($node->tagName, array('table', 'html',
|
||||
'applet', 'caption', 'td', 'th', 'button', 'marquee',
|
||||
'object')) || ($node->tagName === 'foreignObject' &&
|
||||
$node->namespaceURI === self::NS_SVG)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
$this->mode = $this->secondary_mode;
|
||||
}
|
||||
}
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::EOF || (
|
||||
$token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
(in_array($token['name'], array('b', "big", "blockquote", "body", "br",
|
||||
"center", "code", "dc", "dd", "div", "dl", "ds", "dt", "em", "embed", "h1", "h2",
|
||||
"h3", "h4", "h5", "h6", "head", "hr", "i", "img", "li", "listing",
|
||||
"menu", "meta", "nobr", "ol", "p", "pre", "ruby", "s", "small",
|
||||
"span", "strong", "strike", "sub", "sup", "table", "tt", "u", "ul",
|
||||
"var")) || ($token['name'] === 'font' && ($this->getAttr($token, 'color') ||
|
||||
$this->getAttr($token, 'face') || $this->getAttr($token, 'size')))))) {
|
||||
// XERROR: parse error
|
||||
do {
|
||||
$node = array_pop($this->stack);
|
||||
// XDOM
|
||||
} while ($node->namespaceURI !== self::NS_HTML);
|
||||
$this->stack[] = $node;
|
||||
$this->mode = $this->secondary_mode;
|
||||
$this->emitToken($token);
|
||||
} elseif ($token['type'] === HTML5_Tokenizer::STARTTAG) {
|
||||
static $svg_lookup = array(
|
||||
'altglyph' => 'altGlyph',
|
||||
'altglyphdef' => 'altGlyphDef',
|
||||
'altglyphitem' => 'altGlyphItem',
|
||||
'animatecolor' => 'animateColor',
|
||||
'animatemotion' => 'animateMotion',
|
||||
'animatetransform' => 'animateTransform',
|
||||
'clippath' => 'clipPath',
|
||||
'feblend' => 'feBlend',
|
||||
'fecolormatrix' => 'feColorMatrix',
|
||||
'fecomponenttransfer' => 'feComponentTransfer',
|
||||
'fecomposite' => 'feComposite',
|
||||
'feconvolvematrix' => 'feConvolveMatrix',
|
||||
'fediffuselighting' => 'feDiffuseLighting',
|
||||
'fedisplacementmap' => 'feDisplacementMap',
|
||||
'fedistantlight' => 'feDistantLight',
|
||||
'feflood' => 'feFlood',
|
||||
'fefunca' => 'feFuncA',
|
||||
'fefuncb' => 'feFuncB',
|
||||
'fefuncg' => 'feFuncG',
|
||||
'fefuncr' => 'feFuncR',
|
||||
'fegaussianblur' => 'feGaussianBlur',
|
||||
'feimage' => 'feImage',
|
||||
'femerge' => 'feMerge',
|
||||
'femergenode' => 'feMergeNode',
|
||||
'femorphology' => 'feMorphology',
|
||||
'feoffset' => 'feOffset',
|
||||
'fepointlight' => 'fePointLight',
|
||||
'fespecularlighting' => 'feSpecularLighting',
|
||||
'fespotlight' => 'feSpotLight',
|
||||
'fetile' => 'feTile',
|
||||
'feturbulence' => 'feTurbulence',
|
||||
'foreignobject' => 'foreignObject',
|
||||
'glyphref' => 'glyphRef',
|
||||
'lineargradient' => 'linearGradient',
|
||||
'radialgradient' => 'radialGradient',
|
||||
'textpath' => 'textPath',
|
||||
);
|
||||
// XDOM
|
||||
$current = end($this->stack);
|
||||
if ($current->namespaceURI === self::NS_MATHML) {
|
||||
$token = $this->adjustMathMLAttributes($token);
|
||||
}
|
||||
if ($current->namespaceURI === self::NS_SVG &&
|
||||
isset($svg_lookup[$token['name']])) {
|
||||
$token['name'] = $svg_lookup[$token['name']];
|
||||
}
|
||||
if ($current->namespaceURI === self::NS_SVG) {
|
||||
$token = $this->adjustSVGAttributes($token);
|
||||
}
|
||||
$token = $this->adjustForeignAttributes($token);
|
||||
$this->insertForeignElement($token, $current->namespaceURI);
|
||||
if (isset($token['self-closing'])) {
|
||||
array_pop($this->stack);
|
||||
// XERROR: acknowledge self-closing flag
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case self::AFTER_BODY:
|
||||
/* Handle the token as follows: */
|
||||
|
||||
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
|
||||
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
|
||||
or U+0020 SPACE */
|
||||
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
|
||||
/* Process the token as it would be processed if the insertion mode
|
||||
was "in body". */
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
|
||||
/* A comment token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
/* Append a Comment node to the first element in the stack of open
|
||||
elements (the html element), with the data attribute set to the
|
||||
data given in the comment token. */
|
||||
// XDOM
|
||||
$comment = $this->dom->createComment($token['data']);
|
||||
$this->stack[0]->appendChild($comment);
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
// parse error
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
|
||||
/* An end tag with the tag name "html" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'html') {
|
||||
/* If the parser was originally created as part of the HTML
|
||||
* fragment parsing algorithm, this is a parse error; ignore
|
||||
* the token. (fragment case) */
|
||||
$this->ignored = true;
|
||||
// XERROR: implement this
|
||||
|
||||
$this->mode = self::AFTER_AFTER_BODY;
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::EOF) {
|
||||
/* Stop parsing */
|
||||
|
||||
/* Anything else */
|
||||
} else {
|
||||
/* Parse error. Set the insertion mode to "in body" and reprocess
|
||||
the token. */
|
||||
$this->mode = self::IN_BODY;
|
||||
$this->emitToken($token);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_FRAMESET:
|
||||
/* Handle the token as follows: */
|
||||
|
||||
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
|
||||
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
|
||||
U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
|
||||
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
|
||||
/* Append the character to the current node. */
|
||||
$this->insertText($token['data']);
|
||||
|
||||
/* A comment token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
/* Append a Comment node to the current node with the data
|
||||
attribute set to the data given in the comment token. */
|
||||
$this->insertComment($token['data']);
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
// parse error
|
||||
|
||||
/* A start tag with the tag name "frameset" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
$token['name'] === 'frameset') {
|
||||
$this->insertElement($token);
|
||||
|
||||
/* An end tag with the tag name "frameset" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
$token['name'] === 'frameset') {
|
||||
/* If the current node is the root html element, then this is a
|
||||
parse error; ignore the token. (fragment case) */
|
||||
if(end($this->stack)->tagName === 'html') {
|
||||
$this->ignored = true;
|
||||
// Parse error
|
||||
|
||||
} else {
|
||||
/* Otherwise, pop the current node from the stack of open
|
||||
elements. */
|
||||
array_pop($this->stack);
|
||||
|
||||
/* If the parser was not originally created as part of the HTML
|
||||
* fragment parsing algorithm (fragment case), and the current
|
||||
* node is no longer a frameset element, then switch the
|
||||
* insertion mode to "after frameset". */
|
||||
$this->mode = self::AFTER_FRAMESET;
|
||||
}
|
||||
|
||||
/* A start tag with the tag name "frame" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
$token['name'] === 'frame') {
|
||||
/* Insert an HTML element for the token. */
|
||||
$this->insertElement($token);
|
||||
|
||||
/* Immediately pop the current node off the stack of open elements. */
|
||||
array_pop($this->stack);
|
||||
|
||||
// XERROR: Acknowledge the token's self-closing flag, if it is set.
|
||||
|
||||
/* A start tag with the tag name "noframes" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
$token['name'] === 'noframes') {
|
||||
/* Process the token using the rules for the "in head" insertion mode. */
|
||||
$this->processwithRulesFor($token, self::IN_HEAD);
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::EOF) {
|
||||
// XERROR: If the current node is not the root html element, then this is a parse error.
|
||||
/* Stop parsing */
|
||||
/* Anything else */
|
||||
} else {
|
||||
/* Parse error. Ignore the token. */
|
||||
$this->ignored = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case self::AFTER_FRAMESET:
|
||||
/* Handle the token as follows: */
|
||||
|
||||
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
|
||||
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
|
||||
U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
|
||||
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
|
||||
/* Append the character to the current node. */
|
||||
$this->insertText($token['data']);
|
||||
|
||||
/* A comment token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
/* Append a Comment node to the current node with the data
|
||||
attribute set to the data given in the comment token. */
|
||||
$this->insertComment($token['data']);
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
|
||||
// parse error
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
|
||||
/* An end tag with the tag name "html" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
|
||||
$token['name'] === 'html') {
|
||||
$this->mode = self::AFTER_AFTER_FRAMESET;
|
||||
|
||||
/* A start tag with the tag name "noframes" */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
|
||||
$token['name'] === 'noframes') {
|
||||
$this->processWithRulesFor($token, self::IN_HEAD);
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::EOF) {
|
||||
/* Stop parsing */
|
||||
|
||||
/* Anything else */
|
||||
} else {
|
||||
/* Parse error. Ignore the token. */
|
||||
$this->ignored = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case self::AFTER_AFTER_BODY:
|
||||
/* A comment token */
|
||||
if($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
/* Append a Comment node to the Document object with the data
|
||||
attribute set to the data given in the comment token. */
|
||||
// XDOM
|
||||
$comment = $this->dom->createComment($token['data']);
|
||||
$this->dom->appendChild($comment);
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE ||
|
||||
$token['type'] === HTML5_Tokenizer::SPACECHARACTER ||
|
||||
($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html')) {
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
|
||||
/* An end-of-file token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::EOF) {
|
||||
/* OMG DONE!! */
|
||||
} else {
|
||||
// parse error
|
||||
$this->mode = self::IN_BODY;
|
||||
$this->emitToken($token);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::AFTER_AFTER_FRAMESET:
|
||||
/* A comment token */
|
||||
if($token['type'] === HTML5_Tokenizer::COMMENT) {
|
||||
/* Append a Comment node to the Document object with the data
|
||||
attribute set to the data given in the comment token. */
|
||||
// XDOM
|
||||
$comment = $this->dom->createComment($token['data']);
|
||||
$this->dom->appendChild($comment);
|
||||
|
||||
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE ||
|
||||
$token['type'] === HTML5_Tokenizer::SPACECHARACTER ||
|
||||
($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html')) {
|
||||
$this->processWithRulesFor($token, self::IN_BODY);
|
||||
|
||||
/* An end-of-file token */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::EOF) {
|
||||
/* OMG DONE!! */
|
||||
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'nofrmaes') {
|
||||
$this->processWithRulesFor($token, self::IN_HEAD);
|
||||
} else {
|
||||
// parse error
|
||||
}
|
||||
break;
|
||||
}
|
||||
// end funky indenting
|
||||
}
|
||||
|
||||
private function insertElement($token, $append = true) {
|
||||
$el = $this->dom->createElementNS(self::NS_HTML, $token['name']);
|
||||
|
||||
if (!empty($token['attr'])) {
|
||||
foreach($token['attr'] as $attr) {
|
||||
if(!$el->hasAttribute($attr['name'])) {
|
||||
$el->setAttribute($attr['name'], $attr['value']);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($append) {
|
||||
$this->appendToRealParent($el);
|
||||
$this->stack[] = $el;
|
||||
}
|
||||
|
||||
return $el;
|
||||
}
|
||||
|
||||
private function insertText($data) {
|
||||
if ($data === '') return;
|
||||
if ($this->ignore_lf_token) {
|
||||
if ($data[0] === "\n") {
|
||||
$data = substr($data, 1);
|
||||
if ($data === false) return;
|
||||
}
|
||||
}
|
||||
$text = $this->dom->createTextNode($data);
|
||||
$this->appendToRealParent($text);
|
||||
}
|
||||
|
||||
private function insertComment($data) {
|
||||
$comment = $this->dom->createComment($data);
|
||||
$this->appendToRealParent($comment);
|
||||
}
|
||||
|
||||
private function appendToRealParent($node) {
|
||||
// this is only for the foster_parent case
|
||||
/* If the current node is a table, tbody, tfoot, thead, or tr
|
||||
element, then, whenever a node would be inserted into the current
|
||||
node, it must instead be inserted into the foster parent element. */
|
||||
if(!$this->foster_parent || !in_array(end($this->stack)->tagName,
|
||||
array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
|
||||
end($this->stack)->appendChild($node);
|
||||
} else {
|
||||
$this->fosterParent($node);
|
||||
}
|
||||
}
|
||||
|
||||
private function elementInScope($el, $scope = self::SCOPE) {
|
||||
if(is_array($el)) {
|
||||
foreach($el as $element) {
|
||||
if($this->elementInScope($element, $scope)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$leng = count($this->stack);
|
||||
|
||||
for($n = 0; $n < $leng; $n++) {
|
||||
/* 1. Initialise node to be the current node (the bottommost node of
|
||||
the stack). */
|
||||
$node = $this->stack[$leng - 1 - $n];
|
||||
|
||||
if($node->tagName === $el) {
|
||||
/* 2. If node is the target node, terminate in a match state. */
|
||||
return true;
|
||||
|
||||
// We've expanded the logic for these states a little differently;
|
||||
// Hixie's refactoring into "specific scope" is more general, but
|
||||
// this "gets the job done"
|
||||
|
||||
// these are the common states for all scopes
|
||||
} elseif($node->tagName === 'table' || $node->tagName === 'html') {
|
||||
return false;
|
||||
|
||||
// these are valid for "in scope" and "in list item scope"
|
||||
} elseif($scope !== self::SCOPE_TABLE &&
|
||||
(in_array($node->tagName, array('applet', 'caption', 'td',
|
||||
'th', 'button', 'marquee', 'object')) ||
|
||||
$node->tagName === 'foreignObject' && $node->namespaceURI === self::NS_SVG)) {
|
||||
return false;
|
||||
|
||||
|
||||
// these are valid for "in list item scope"
|
||||
} elseif($scope === self::SCOPE_LISTITEM && in_array($node->tagName, array('ol', 'ul'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Otherwise, set node to the previous entry in the stack of open
|
||||
elements and return to step 2. (This will never fail, since the loop
|
||||
will always terminate in the previous step if the top of the stack
|
||||
is reached.) */
|
||||
}
|
||||
}
|
||||
|
||||
private function reconstructActiveFormattingElements() {
|
||||
/* 1. If there are no entries in the list of active formatting elements,
|
||||
then there is nothing to reconstruct; stop this algorithm. */
|
||||
$formatting_elements = count($this->a_formatting);
|
||||
|
||||
if($formatting_elements === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 3. Let entry be the last (most recently added) element in the list
|
||||
of active formatting elements. */
|
||||
$entry = end($this->a_formatting);
|
||||
|
||||
/* 2. If the last (most recently added) entry in the list of active
|
||||
formatting elements is a marker, or if it is an element that is in the
|
||||
stack of open elements, then there is nothing to reconstruct; stop this
|
||||
algorithm. */
|
||||
if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for($a = $formatting_elements - 1; $a >= 0; true) {
|
||||
/* 4. If there are no entries before entry in the list of active
|
||||
formatting elements, then jump to step 8. */
|
||||
if($a === 0) {
|
||||
$step_seven = false;
|
||||
break;
|
||||
}
|
||||
|
||||
/* 5. Let entry be the entry one earlier than entry in the list of
|
||||
active formatting elements. */
|
||||
$a--;
|
||||
$entry = $this->a_formatting[$a];
|
||||
|
||||
/* 6. If entry is neither a marker nor an element that is also in
|
||||
thetack of open elements, go to step 4. */
|
||||
if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while(true) {
|
||||
/* 7. Let entry be the element one later than entry in the list of
|
||||
active formatting elements. */
|
||||
if(isset($step_seven) && $step_seven === true) {
|
||||
$a++;
|
||||
$entry = $this->a_formatting[$a];
|
||||
}
|
||||
|
||||
/* 8. Perform a shallow clone of the element entry to obtain clone. */
|
||||
$clone = $entry->cloneNode();
|
||||
|
||||
/* 9. Append clone to the current node and push it onto the stack
|
||||
of open elements so that it is the new current node. */
|
||||
$this->appendToRealParent($clone);
|
||||
$this->stack[] = $clone;
|
||||
|
||||
/* 10. Replace the entry for entry in the list with an entry for
|
||||
clone. */
|
||||
$this->a_formatting[$a] = $clone;
|
||||
|
||||
/* 11. If the entry for clone in the list of active formatting
|
||||
elements is not the last entry in the list, return to step 7. */
|
||||
if(end($this->a_formatting) !== $clone) {
|
||||
$step_seven = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function clearTheActiveFormattingElementsUpToTheLastMarker() {
|
||||
/* When the steps below require the UA to clear the list of active
|
||||
formatting elements up to the last marker, the UA must perform the
|
||||
following steps: */
|
||||
|
||||
while(true) {
|
||||
/* 1. Let entry be the last (most recently added) entry in the list
|
||||
of active formatting elements. */
|
||||
$entry = end($this->a_formatting);
|
||||
|
||||
/* 2. Remove entry from the list of active formatting elements. */
|
||||
array_pop($this->a_formatting);
|
||||
|
||||
/* 3. If entry was a marker, then stop the algorithm at this point.
|
||||
The list has been cleared up to the last marker. */
|
||||
if($entry === self::MARKER) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function generateImpliedEndTags($exclude = array()) {
|
||||
/* When the steps below require the UA to generate implied end tags,
|
||||
* then, while the current node is a dc element, a dd element, a ds
|
||||
* element, a dt element, an li element, an option element, an optgroup
|
||||
* element, a p element, an rp element, or an rt element, the UA must
|
||||
* pop the current node off the stack of open elements. */
|
||||
$node = end($this->stack);
|
||||
$elements = array_diff(array('dc', 'dd', 'ds', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude);
|
||||
|
||||
while(in_array(end($this->stack)->tagName, $elements)) {
|
||||
array_pop($this->stack);
|
||||
}
|
||||
}
|
||||
|
||||
private function getElementCategory($node) {
|
||||
if (!is_object($node)) debug_print_backtrace();
|
||||
$name = $node->tagName;
|
||||
if(in_array($name, $this->special))
|
||||
return self::SPECIAL;
|
||||
|
||||
elseif(in_array($name, $this->scoping))
|
||||
return self::SCOPING;
|
||||
|
||||
elseif(in_array($name, $this->formatting))
|
||||
return self::FORMATTING;
|
||||
|
||||
else
|
||||
return self::PHRASING;
|
||||
}
|
||||
|
||||
private function clearStackToTableContext($elements) {
|
||||
/* When the steps above require the UA to clear the stack back to a
|
||||
table context, it means that the UA must, while the current node is not
|
||||
a table element or an html element, pop elements from the stack of open
|
||||
elements. */
|
||||
while(true) {
|
||||
$name = end($this->stack)->tagName;
|
||||
|
||||
if(in_array($name, $elements)) {
|
||||
break;
|
||||
} else {
|
||||
array_pop($this->stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetInsertionMode($context = null) {
|
||||
/* 1. Let last be false. */
|
||||
$last = false;
|
||||
$leng = count($this->stack);
|
||||
|
||||
for($n = $leng - 1; $n >= 0; $n--) {
|
||||
/* 2. Let node be the last node in the stack of open elements. */
|
||||
$node = $this->stack[$n];
|
||||
|
||||
/* 3. If node is the first node in the stack of open elements, then
|
||||
* set last to true and set node to the context element. (fragment
|
||||
* case) */
|
||||
if($this->stack[0]->isSameNode($node)) {
|
||||
$last = true;
|
||||
$node = $context;
|
||||
}
|
||||
|
||||
/* 4. If node is a select element, then switch the insertion mode to
|
||||
"in select" and abort these steps. (fragment case) */
|
||||
if($node->tagName === 'select') {
|
||||
$this->mode = self::IN_SELECT;
|
||||
break;
|
||||
|
||||
/* 5. If node is a td or th element, then switch the insertion mode
|
||||
to "in cell" and abort these steps. */
|
||||
} elseif($node->tagName === 'td' || $node->nodeName === 'th') {
|
||||
$this->mode = self::IN_CELL;
|
||||
break;
|
||||
|
||||
/* 6. If node is a tr element, then switch the insertion mode to
|
||||
"in row" and abort these steps. */
|
||||
} elseif($node->tagName === 'tr') {
|
||||
$this->mode = self::IN_ROW;
|
||||
break;
|
||||
|
||||
/* 7. If node is a tbody, thead, or tfoot element, then switch the
|
||||
insertion mode to "in table body" and abort these steps. */
|
||||
} elseif(in_array($node->tagName, array('tbody', 'thead', 'tfoot'))) {
|
||||
$this->mode = self::IN_TABLE_BODY;
|
||||
break;
|
||||
|
||||
/* 8. If node is a caption element, then switch the insertion mode
|
||||
to "in caption" and abort these steps. */
|
||||
} elseif($node->tagName === 'caption') {
|
||||
$this->mode = self::IN_CAPTION;
|
||||
break;
|
||||
|
||||
/* 9. If node is a colgroup element, then switch the insertion mode
|
||||
to "in column group" and abort these steps. (innerHTML case) */
|
||||
} elseif($node->tagName === 'colgroup') {
|
||||
$this->mode = self::IN_COLUMN_GROUP;
|
||||
break;
|
||||
|
||||
/* 10. If node is a table element, then switch the insertion mode
|
||||
to "in table" and abort these steps. */
|
||||
} elseif($node->tagName === 'table') {
|
||||
$this->mode = self::IN_TABLE;
|
||||
break;
|
||||
|
||||
/* 11. If node is an element from the MathML namespace or the SVG
|
||||
* namespace, then switch the insertion mode to "in foreign
|
||||
* content", let the secondary insertion mode be "in body", and
|
||||
* abort these steps. */
|
||||
} elseif($node->namespaceURI === self::NS_SVG ||
|
||||
$node->namespaceURI === self::NS_MATHML) {
|
||||
$this->mode = self::IN_FOREIGN_CONTENT;
|
||||
$this->secondary_mode = self::IN_BODY;
|
||||
break;
|
||||
|
||||
/* 12. If node is a head element, then switch the insertion mode
|
||||
to "in body" ("in body"! not "in head"!) and abort these steps.
|
||||
(fragment case) */
|
||||
} elseif($node->tagName === 'head') {
|
||||
$this->mode = self::IN_BODY;
|
||||
break;
|
||||
|
||||
/* 13. If node is a body element, then switch the insertion mode to
|
||||
"in body" and abort these steps. */
|
||||
} elseif($node->tagName === 'body') {
|
||||
$this->mode = self::IN_BODY;
|
||||
break;
|
||||
|
||||
/* 14. If node is a frameset element, then switch the insertion
|
||||
mode to "in frameset" and abort these steps. (fragment case) */
|
||||
} elseif($node->tagName === 'frameset') {
|
||||
$this->mode = self::IN_FRAMESET;
|
||||
break;
|
||||
|
||||
/* 15. If node is an html element, then: if the head element
|
||||
pointer is null, switch the insertion mode to "before head",
|
||||
otherwise, switch the insertion mode to "after head". In either
|
||||
case, abort these steps. (fragment case) */
|
||||
} elseif($node->tagName === 'html') {
|
||||
$this->mode = ($this->head_pointer === null)
|
||||
? self::BEFORE_HEAD
|
||||
: self::AFTER_HEAD;
|
||||
|
||||
break;
|
||||
|
||||
/* 16. If last is true, then set the insertion mode to "in body"
|
||||
and abort these steps. (fragment case) */
|
||||
} elseif($last) {
|
||||
$this->mode = self::IN_BODY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function closeCell() {
|
||||
/* If the stack of open elements has a td or th element in table scope,
|
||||
then act as if an end tag token with that tag name had been seen. */
|
||||
foreach(array('td', 'th') as $cell) {
|
||||
if($this->elementInScope($cell, self::SCOPE_TABLE)) {
|
||||
$this->emitToken(array(
|
||||
'name' => $cell,
|
||||
'type' => HTML5_Tokenizer::ENDTAG
|
||||
));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function processWithRulesFor($token, $mode) {
|
||||
/* "using the rules for the m insertion mode", where m is one of these
|
||||
* modes, the user agent must use the rules described under the m
|
||||
* insertion mode's section, but must leave the insertion mode
|
||||
* unchanged unless the rules in m themselves switch the insertion mode
|
||||
* to a new value. */
|
||||
return $this->emitToken($token, $mode);
|
||||
}
|
||||
|
||||
private function insertCDATAElement($token) {
|
||||
$this->insertElement($token);
|
||||
$this->original_mode = $this->mode;
|
||||
$this->mode = self::IN_CDATA_RCDATA;
|
||||
$this->content_model = HTML5_Tokenizer::CDATA;
|
||||
}
|
||||
|
||||
private function insertRCDATAElement($token) {
|
||||
$this->insertElement($token);
|
||||
$this->original_mode = $this->mode;
|
||||
$this->mode = self::IN_CDATA_RCDATA;
|
||||
$this->content_model = HTML5_Tokenizer::RCDATA;
|
||||
}
|
||||
|
||||
private function getAttr($token, $key) {
|
||||
if (!isset($token['attr'])) return false;
|
||||
$ret = false;
|
||||
foreach ($token['attr'] as $keypair) {
|
||||
if ($keypair['name'] === $key) $ret = $keypair['value'];
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
private function getCurrentTable() {
|
||||
/* The current table is the last table element in the stack of open
|
||||
* elements, if there is one. If there is no table element in the stack
|
||||
* of open elements (fragment case), then the current table is the
|
||||
* first element in the stack of open elements (the html element). */
|
||||
for ($i = count($this->stack) - 1; $i >= 0; $i--) {
|
||||
if ($this->stack[$i]->tagName === 'table') {
|
||||
return $this->stack[$i];
|
||||
}
|
||||
}
|
||||
return $this->stack[0];
|
||||
}
|
||||
|
||||
private function getFosterParent() {
|
||||
/* The foster parent element is the parent element of the last
|
||||
table element in the stack of open elements, if there is a
|
||||
table element and it has such a parent element. If there is no
|
||||
table element in the stack of open elements (innerHTML case),
|
||||
then the foster parent element is the first element in the
|
||||
stack of open elements (the html element). Otherwise, if there
|
||||
is a table element in the stack of open elements, but the last
|
||||
table element in the stack of open elements has no parent, or
|
||||
its parent node is not an element, then the foster parent
|
||||
element is the element before the last table element in the
|
||||
stack of open elements. */
|
||||
for($n = count($this->stack) - 1; $n >= 0; $n--) {
|
||||
if($this->stack[$n]->tagName === 'table') {
|
||||
$table = $this->stack[$n];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($table) && $table->parentNode !== null) {
|
||||
return $table->parentNode;
|
||||
|
||||
} elseif(!isset($table)) {
|
||||
return $this->stack[0];
|
||||
|
||||
} elseif(isset($table) && ($table->parentNode === null ||
|
||||
$table->parentNode->nodeType !== XML_ELEMENT_NODE)) {
|
||||
return $this->stack[$n - 1];
|
||||
}
|
||||
}
|
||||
|
||||
public function fosterParent($node) {
|
||||
$foster_parent = $this->getFosterParent();
|
||||
$table = $this->getCurrentTable(); // almost equivalent to last table element, except it can be html
|
||||
/* When a node node is to be foster parented, the node node must be
|
||||
* be inserted into the foster parent element. */
|
||||
/* If the foster parent element is the parent element of the last table
|
||||
* element in the stack of open elements, then node must be inserted
|
||||
* immediately before the last table element in the stack of open
|
||||
* elements in the foster parent element; otherwise, node must be
|
||||
* appended to the foster parent element. */
|
||||
if ($table->tagName === 'table' && $table->parentNode->isSameNode($foster_parent)) {
|
||||
$foster_parent->insertBefore($node, $table);
|
||||
} else {
|
||||
$foster_parent->appendChild($node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For debugging, prints the stack
|
||||
*/
|
||||
private function printStack() {
|
||||
$names = array();
|
||||
foreach ($this->stack as $i => $element) {
|
||||
$names[] = $element->tagName;
|
||||
}
|
||||
echo " -> stack [" . implode(', ', $names) . "]\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* For debugging, prints active formatting elements
|
||||
*/
|
||||
private function printActiveFormattingElements() {
|
||||
if (!$this->a_formatting) return;
|
||||
$names = array();
|
||||
foreach ($this->a_formatting as $node) {
|
||||
if ($node === self::MARKER) $names[] = 'MARKER';
|
||||
else $names[] = $node->tagName;
|
||||
}
|
||||
echo " -> active formatting [" . implode(', ', $names) . "]\n";
|
||||
}
|
||||
|
||||
public function currentTableIsTainted() {
|
||||
return !empty($this->getCurrentTable()->tainted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the tree constructor for building a fragment.
|
||||
*/
|
||||
public function setupContext($context = null) {
|
||||
$this->fragment = true;
|
||||
if ($context) {
|
||||
$context = $this->dom->createElementNS(self::NS_HTML, $context);
|
||||
/* 4.1. Set the HTML parser's tokenization stage's content model
|
||||
* flag according to the context element, as follows: */
|
||||
switch ($context->tagName) {
|
||||
case 'title': case 'textarea':
|
||||
$this->content_model = HTML5_Tokenizer::RCDATA;
|
||||
break;
|
||||
case 'style': case 'script': case 'xmp': case 'iframe':
|
||||
case 'noembed': case 'noframes':
|
||||
$this->content_model = HTML5_Tokenizer::CDATA;
|
||||
break;
|
||||
case 'noscript':
|
||||
// XSCRIPT: assuming scripting is enabled
|
||||
$this->content_model = HTML5_Tokenizer::CDATA;
|
||||
break;
|
||||
case 'plaintext':
|
||||
$this->content_model = HTML5_Tokenizer::PLAINTEXT;
|
||||
break;
|
||||
}
|
||||
/* 4.2. Let root be a new html element with no attributes. */
|
||||
$root = $this->dom->createElementNS(self::NS_HTML, 'html');
|
||||
$this->root = $root;
|
||||
/* 4.3 Append the element root to the Document node created above. */
|
||||
$this->dom->appendChild($root);
|
||||
/* 4.4 Set up the parser's stack of open elements so that it
|
||||
* contains just the single element root. */
|
||||
$this->stack = array($root);
|
||||
/* 4.5 Reset the parser's insertion mode appropriately. */
|
||||
$this->resetInsertionMode($context);
|
||||
/* 4.6 Set the parser's form element pointer to the nearest node
|
||||
* to the context element that is a form element (going straight up
|
||||
* the ancestor chain, and including the element itself, if it is a
|
||||
* form element), or, if there is no such form element, to null. */
|
||||
$node = $context;
|
||||
do {
|
||||
if ($node->tagName === 'form') {
|
||||
$this->form_pointer = $node;
|
||||
break;
|
||||
}
|
||||
} while ($node = $node->parentNode);
|
||||
}
|
||||
}
|
||||
|
||||
public function adjustMathMLAttributes($token) {
|
||||
foreach ($token['attr'] as &$kp) {
|
||||
if ($kp['name'] === 'definitionurl') {
|
||||
$kp['name'] = 'definitionURL';
|
||||
}
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function adjustSVGAttributes($token) {
|
||||
static $lookup = array(
|
||||
'attributename' => 'attributeName',
|
||||
'attributetype' => 'attributeType',
|
||||
'basefrequency' => 'baseFrequency',
|
||||
'baseprofile' => 'baseProfile',
|
||||
'calcmode' => 'calcMode',
|
||||
'clippathunits' => 'clipPathUnits',
|
||||
'contentscripttype' => 'contentScriptType',
|
||||
'contentstyletype' => 'contentStyleType',
|
||||
'diffuseconstant' => 'diffuseConstant',
|
||||
'edgemode' => 'edgeMode',
|
||||
'externalresourcesrequired' => 'externalResourcesRequired',
|
||||
'filterres' => 'filterRes',
|
||||
'filterunits' => 'filterUnits',
|
||||
'glyphref' => 'glyphRef',
|
||||
'gradienttransform' => 'gradientTransform',
|
||||
'gradientunits' => 'gradientUnits',
|
||||
'kernelmatrix' => 'kernelMatrix',
|
||||
'kernelunitlength' => 'kernelUnitLength',
|
||||
'keypoints' => 'keyPoints',
|
||||
'keysplines' => 'keySplines',
|
||||
'keytimes' => 'keyTimes',
|
||||
'lengthadjust' => 'lengthAdjust',
|
||||
'limitingconeangle' => 'limitingConeAngle',
|
||||
'markerheight' => 'markerHeight',
|
||||
'markerunits' => 'markerUnits',
|
||||
'markerwidth' => 'markerWidth',
|
||||
'maskcontentunits' => 'maskContentUnits',
|
||||
'maskunits' => 'maskUnits',
|
||||
'numoctaves' => 'numOctaves',
|
||||
'pathlength' => 'pathLength',
|
||||
'patterncontentunits' => 'patternContentUnits',
|
||||
'patterntransform' => 'patternTransform',
|
||||
'patternunits' => 'patternUnits',
|
||||
'pointsatx' => 'pointsAtX',
|
||||
'pointsaty' => 'pointsAtY',
|
||||
'pointsatz' => 'pointsAtZ',
|
||||
'preservealpha' => 'preserveAlpha',
|
||||
'preserveaspectratio' => 'preserveAspectRatio',
|
||||
'primitiveunits' => 'primitiveUnits',
|
||||
'refx' => 'refX',
|
||||
'refy' => 'refY',
|
||||
'repeatcount' => 'repeatCount',
|
||||
'repeatdur' => 'repeatDur',
|
||||
'requiredextensions' => 'requiredExtensions',
|
||||
'requiredfeatures' => 'requiredFeatures',
|
||||
'specularconstant' => 'specularConstant',
|
||||
'specularexponent' => 'specularExponent',
|
||||
'spreadmethod' => 'spreadMethod',
|
||||
'startoffset' => 'startOffset',
|
||||
'stddeviation' => 'stdDeviation',
|
||||
'stitchtiles' => 'stitchTiles',
|
||||
'surfacescale' => 'surfaceScale',
|
||||
'systemlanguage' => 'systemLanguage',
|
||||
'tablevalues' => 'tableValues',
|
||||
'targetx' => 'targetX',
|
||||
'targety' => 'targetY',
|
||||
'textlength' => 'textLength',
|
||||
'viewbox' => 'viewBox',
|
||||
'viewtarget' => 'viewTarget',
|
||||
'xchannelselector' => 'xChannelSelector',
|
||||
'ychannelselector' => 'yChannelSelector',
|
||||
'zoomandpan' => 'zoomAndPan',
|
||||
);
|
||||
foreach ($token['attr'] as &$kp) {
|
||||
if (isset($lookup[$kp['name']])) {
|
||||
$kp['name'] = $lookup[$kp['name']];
|
||||
}
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function adjustForeignAttributes($token) {
|
||||
static $lookup = array(
|
||||
'xlink:actuate' => array('xlink', 'actuate', self::NS_XLINK),
|
||||
'xlink:arcrole' => array('xlink', 'arcrole', self::NS_XLINK),
|
||||
'xlink:href' => array('xlink', 'href', self::NS_XLINK),
|
||||
'xlink:role' => array('xlink', 'role', self::NS_XLINK),
|
||||
'xlink:show' => array('xlink', 'show', self::NS_XLINK),
|
||||
'xlink:title' => array('xlink', 'title', self::NS_XLINK),
|
||||
'xlink:type' => array('xlink', 'type', self::NS_XLINK),
|
||||
'xml:base' => array('xml', 'base', self::NS_XML),
|
||||
'xml:lang' => array('xml', 'lang', self::NS_XML),
|
||||
'xml:space' => array('xml', 'space', self::NS_XML),
|
||||
'xmlns' => array(null, 'xmlns', self::NS_XMLNS),
|
||||
'xmlns:xlink' => array('xmlns', 'xlink', self::NS_XMLNS),
|
||||
);
|
||||
foreach ($token['attr'] as &$kp) {
|
||||
if (isset($lookup[$kp['name']])) {
|
||||
$kp['name'] = $lookup[$kp['name']];
|
||||
}
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function insertForeignElement($token, $namespaceURI) {
|
||||
$el = $this->dom->createElementNS($namespaceURI, $token['name']);
|
||||
if (!empty($token['attr'])) {
|
||||
foreach ($token['attr'] as $kp) {
|
||||
$attr = $kp['name'];
|
||||
if (is_array($attr)) {
|
||||
$ns = $attr[2];
|
||||
$attr = $attr[1];
|
||||
} else {
|
||||
$ns = self::NS_HTML;
|
||||
}
|
||||
if (!$el->hasAttributeNS($ns, $attr)) {
|
||||
// XSKETCHY: work around godawful libxml bug
|
||||
if ($ns === self::NS_XLINK) {
|
||||
$el->setAttribute('xlink:'.$attr, $kp['value']);
|
||||
} elseif ($ns === self::NS_HTML) {
|
||||
// Another godawful libxml bug
|
||||
$el->setAttribute($attr, $kp['value']);
|
||||
} else {
|
||||
$el->setAttributeNS($ns, $attr, $kp['value']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->appendToRealParent($el);
|
||||
$this->stack[] = $el;
|
||||
// XERROR: see below
|
||||
/* If the newly created element has an xmlns attribute in the XMLNS
|
||||
* namespace whose value is not exactly the same as the element's
|
||||
* namespace, that is a parse error. Similarly, if the newly created
|
||||
* element has an xmlns:xlink attribute in the XMLNS namespace whose
|
||||
* value is not the XLink Namespace, that is a parse error. */
|
||||
}
|
||||
|
||||
public function save() {
|
||||
$this->dom->normalize();
|
||||
if (!$this->fragment) {
|
||||
return $this->dom;
|
||||
} else {
|
||||
if ($this->root) {
|
||||
return $this->root->childNodes;
|
||||
} else {
|
||||
return $this->dom->childNodes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1
thirdparty/html5lib/HTML5/named-character-references.ser
vendored
Normal file
1
thirdparty/html5lib/HTML5/named-character-references.ser
vendored
Normal file
File diff suppressed because one or more lines are too long
22
thirdparty/html5lib/LICENSE
vendored
Normal file
22
thirdparty/html5lib/LICENSE
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
Copyright (c) 2006-2011 The Authors
|
||||
|
||||
Contributors:
|
||||
James Graham - jg307@cam.ac.uk
|
||||
Anne van Kesteren - annevankesteren@gmail.com
|
||||
Lachlan Hunt - lachlan.hunt@lachy.id.au
|
||||
Matt McDonald - kanashii@kanashii.ca
|
||||
Sam Ruby - rubys@intertwingly.net
|
||||
Ian Hickson (Google) - ian@hixie.ch
|
||||
Thomas Broyer - t.broyer@ltgt.net
|
||||
Jacques Distler - distler@golem.ph.utexas.edu
|
||||
Henri Sivonen - hsivonen@iki.fi
|
||||
Adam Barth - abarth@webkit.org
|
||||
Eric Seidel - eric@webkit.org
|
||||
The Mozilla Foundation (contributions from Henri Sivonen since 2008)
|
||||
David Flanagan (Mozilla) - dflanagan@mozilla.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
47
thirdparty/html5lib/README
vendored
Normal file
47
thirdparty/html5lib/README
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
html5lib - php flavour
|
||||
|
||||
This is an implementation of the tokenization and tree-building parts
|
||||
of the HTML5 specification in PHP. Potential uses of this library
|
||||
can be found in web-scrapers and HTML filters.
|
||||
|
||||
Warning: This is a pre-alpha release, and as such, certain parts of
|
||||
this code are not up-to-snuff (e.g. error reporting and performance).
|
||||
However, the code is very close to spec and passes 100% of tests
|
||||
not related to parse errors. Nevertheless, expect to have to update
|
||||
your code on the next upgrade.
|
||||
|
||||
|
||||
Usage notes:
|
||||
|
||||
<?php
|
||||
require_once '/path/to/HTML5/Parser.php';
|
||||
$dom = HTML5_Parser::parse('<html><body>...');
|
||||
$nodelist = HTML5_Parser::parseFragment('<b>Boo</b><br>');
|
||||
$nodelist = HTML5_Parser::parseFragment('<td>Bar</td>', 'table');
|
||||
|
||||
|
||||
Documentation:
|
||||
|
||||
HTML5_Parser::parse($text)
|
||||
$text : HTML to parse
|
||||
return : DOMDocument of parsed document
|
||||
|
||||
HTML5_Parser::parseFragment($text, $context)
|
||||
$text : HTML to parse
|
||||
$context : String name of context element
|
||||
return : DOMDocument of parsed document
|
||||
|
||||
|
||||
Developer notes:
|
||||
|
||||
* To setup unit tests, you need to add a small stub file test-settings.php
|
||||
that contains $simpletest_location = 'path/to/simpletest/'; This needs to
|
||||
be version 1.1 (or, until that is released, SVN trunk) of SimpleTest.
|
||||
|
||||
* We don't want to ultimately use PHP's DOM because it is not tolerant
|
||||
of certain types of errors that HTML 5 allows (for example, an element
|
||||
"foo@bar"). But the current implementation uses it, since it's easy.
|
||||
Eventually, this html5lib implementation will get a version of SimpleTree;
|
||||
and may possibly start using that by default.
|
||||
|
||||
vim: et sw=4 sts=4
|
@ -38,7 +38,20 @@
|
||||
ed.addCommand('ssmedia', function(ed) {
|
||||
jQuery('#' + this.id).entwine('ss').openMediaDialog();
|
||||
});
|
||||
|
||||
|
||||
// Replace the mceAdvLink and mceLink commands with the sslink command, and
|
||||
// the mceAdvImage and mceImage commands with the ssmedia command
|
||||
ed.onBeforeExecCommand.add(function(ed, cmd, ui, val, a){
|
||||
if (cmd == 'mceAdvLink' || cmd == 'mceLink'){
|
||||
ed.execCommand('sslink', ui, val, a);
|
||||
a.terminate = true;
|
||||
}
|
||||
else if (cmd == 'mceAdvImage' || cmd == 'mceImage'){
|
||||
ed.execCommand('ssmedia', ui, val, a);
|
||||
a.terminate = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Disable link button when no link is selected
|
||||
ed.onNodeChange.add(function(ed, cm, n, co) {
|
||||
cm.setDisabled('sslink', co && n.nodeName != 'A');
|
||||
|
5
thirdparty/tinymce_ssbuttons/langs/nl.js
vendored
Normal file
5
thirdparty/tinymce_ssbuttons/langs/nl.js
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
tinyMCE.addI18n('nl.tinymce_ssbuttons', {
|
||||
insertlink: 'Link toevoegen',
|
||||
insertmedia: 'Media toevoegen',
|
||||
insertflash: 'Flash Object toevoegen'
|
||||
});
|
3
thirdparty/tinymce_ssmacron/langs/nl.js
vendored
Normal file
3
thirdparty/tinymce_ssmacron/langs/nl.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
tinyMCE.addI18n('nl.tinymce_ssmacron', {
|
||||
insertmacron: 'Een macron toevoegen'
|
||||
});
|
@ -246,9 +246,10 @@ class Requirements {
|
||||
*
|
||||
* @param string $combinedFileName
|
||||
* @param array $files
|
||||
* @param string $media
|
||||
*/
|
||||
public static function combine_files($combinedFileName, $files) {
|
||||
self::backend()->combine_files($combinedFileName, $files);
|
||||
public static function combine_files($combinedFileName, $files, $media = null) {
|
||||
self::backend()->combine_files($combinedFileName, $files, $media);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -871,8 +872,9 @@ class Requirements_Backend {
|
||||
* @param string $combinedFileName Filename of the combined file (will be stored in {@link Director::baseFolder()}
|
||||
* by default)
|
||||
* @param array $files Array of filenames relative to the webroot
|
||||
* @param string $media Comma-separated list of media-types (e.g. "screen,projector").
|
||||
*/
|
||||
public function combine_files($combinedFileName, $files) {
|
||||
public function combine_files($combinedFileName, $files, $media = null) {
|
||||
// duplicate check
|
||||
foreach($this->combine_files as $_combinedFileName => $_files) {
|
||||
$duplicates = array_intersect($_files, $files);
|
||||
@ -889,7 +891,7 @@ class Requirements_Backend {
|
||||
if (isset($file['type']) && in_array($file['type'], array('css', 'javascript', 'js'))) {
|
||||
switch ($file['type']) {
|
||||
case 'css':
|
||||
$this->css($file['path']);
|
||||
$this->css($file['path'], $media);
|
||||
break;
|
||||
default:
|
||||
$this->javascript($file['path']);
|
||||
@ -899,7 +901,7 @@ class Requirements_Backend {
|
||||
} elseif (isset($file[1]) && in_array($file[1], array('css', 'javascript', 'js'))) {
|
||||
switch ($file[1]) {
|
||||
case 'css':
|
||||
$this->css($file[0]);
|
||||
$this->css($file[0], $media);
|
||||
break;
|
||||
default:
|
||||
$this->javascript($file[0]);
|
||||
@ -914,7 +916,7 @@ class Requirements_Backend {
|
||||
if(substr($file, -2) == 'js') {
|
||||
$this->javascript($file);
|
||||
} elseif(substr($file, -3) == 'css') {
|
||||
$this->css($file);
|
||||
$this->css($file, $media);
|
||||
} else {
|
||||
user_error("Requirements_Backend::combine_files(): Couldn't guess file type for file '$file', "
|
||||
. "please specify by passing using an array instead.", E_USER_NOTICE);
|
||||
@ -998,7 +1000,8 @@ class Requirements_Backend {
|
||||
|
||||
foreach($this->css as $file => $params) {
|
||||
if(isset($combinerCheck[$file])) {
|
||||
$newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = true;
|
||||
// Inherit the parameters from the last file in the combine set.
|
||||
$newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = $params;
|
||||
$combinedFiles[$combinerCheck[$file]] = true;
|
||||
} else {
|
||||
$newCSSRequirements[$file] = $params;
|
||||
@ -1109,7 +1112,7 @@ class Requirements_Backend {
|
||||
$this->css($path.$css, $media);
|
||||
}
|
||||
else if ($module) {
|
||||
$this->css($module.$css);
|
||||
$this->css($module.$css, $media);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -646,7 +646,8 @@ class SSTemplateParser extends Parser {
|
||||
}
|
||||
|
||||
|
||||
/* Translate: "<%t" < Entity < (Default:QuotedString)? < (!("is" "=") < "is" < Context:QuotedString)? < (InjectionVariables)? > "%>" */
|
||||
/* Translate: "<%t" < Entity < (Default:QuotedString)? < (!("is" "=") < "is" < Context:QuotedString)? <
|
||||
(InjectionVariables)? > "%>" */
|
||||
protected $match_Translate_typestack = array('Translate');
|
||||
function match_Translate ($stack = array()) {
|
||||
$matchrule = "Translate"; $result = $this->construct($matchrule, $matchrule, null);
|
||||
@ -1671,7 +1672,7 @@ class SSTemplateParser extends Parser {
|
||||
function Require_Call(&$res, $sub) {
|
||||
$res['php'] = "Requirements::".$sub['Method']['text'].'('.$sub['CallArguments']['php'].');';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* CacheBlockArgument:
|
||||
!( "if " | "unless " )
|
||||
@ -2914,7 +2915,7 @@ class SSTemplateParser extends Parser {
|
||||
function OldTTag_OldTPart(&$res, $sub) {
|
||||
$res['php'] = $sub['php'];
|
||||
}
|
||||
|
||||
|
||||
/* OldSprintfTag: "<%" < "sprintf" < "(" < OldTPart < "," < CallArguments > ")" > "%>" */
|
||||
protected $match_OldSprintfTag_typestack = array('OldSprintfTag');
|
||||
function match_OldSprintfTag ($stack = array()) {
|
||||
@ -3052,8 +3053,19 @@ class SSTemplateParser extends Parser {
|
||||
}
|
||||
|
||||
function NamedArgument_Value(&$res, $sub) {
|
||||
$res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php']
|
||||
: str_replace('$$FINAL', 'XML_val', $sub['php']);
|
||||
switch($sub['ArgumentMode']) {
|
||||
case 'string':
|
||||
$res['php'] .= $sub['php'];
|
||||
break;
|
||||
|
||||
case 'default':
|
||||
$res['php'] .= $sub['string_php'];
|
||||
break;
|
||||
|
||||
default:
|
||||
$res['php'] .= str_replace('$$FINAL', 'obj', $sub['php']) . '->self()';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Include: "<%" < "include" < Template:Word < (NamedArgument ( < "," < NamedArgument )*)? > "%>" */
|
||||
@ -3642,7 +3654,8 @@ class SSTemplateParser extends Parser {
|
||||
$method = 'OpenBlock_Handle_'.$blockname;
|
||||
if (method_exists($this, $method)) $res['php'] = $this->$method($res);
|
||||
else {
|
||||
throw new SSTemplateParseException('Unknown open block "'.$blockname.'" encountered. Perhaps you missed the closing tag or have mis-spelled it?', $this);
|
||||
throw new SSTemplateParseException('Unknown open block "'.$blockname.'" encountered. Perhaps you missed ' .
|
||||
' the closing tag or have mis-spelled it?', $this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3711,7 +3724,8 @@ class SSTemplateParser extends Parser {
|
||||
|
||||
function MismatchedEndBlock__finalise(&$res) {
|
||||
$blockname = $res['Word']['text'];
|
||||
throw new SSTemplateParseException('Unexpected close tag end_'.$blockname.' encountered. Perhaps you have mis-nested blocks, or have mis-spelled a tag?', $this);
|
||||
throw new SSTemplateParseException('Unexpected close tag end_' . $blockname .
|
||||
' encountered. Perhaps you have mis-nested blocks, or have mis-spelled a tag?', $this);
|
||||
}
|
||||
|
||||
/* MalformedOpenTag: '<%' < !NotBlockTag Tag:Word !( ( [ :BlockArguments ] )? > '%>' ) */
|
||||
@ -3796,7 +3810,8 @@ class SSTemplateParser extends Parser {
|
||||
|
||||
function MalformedOpenTag__finalise(&$res) {
|
||||
$tag = $res['Tag']['text'];
|
||||
throw new SSTemplateParseException("Malformed opening block tag $tag. Perhaps you have tried to use operators?", $this);
|
||||
throw new SSTemplateParseException("Malformed opening block tag $tag. Perhaps you have tried to use operators?"
|
||||
, $this);
|
||||
}
|
||||
|
||||
/* MalformedCloseTag: '<%' < Tag:('end_' :Word ) !( > '%>' ) */
|
||||
@ -3860,7 +3875,8 @@ class SSTemplateParser extends Parser {
|
||||
|
||||
function MalformedCloseTag__finalise(&$res) {
|
||||
$tag = $res['Tag']['text'];
|
||||
throw new SSTemplateParseException("Malformed closing block tag $tag. Perhaps you have tried to pass an argument to one?", $this);
|
||||
throw new SSTemplateParseException("Malformed closing block tag $tag. Perhaps you have tried to pass an " .
|
||||
"argument to one?", $this);
|
||||
}
|
||||
|
||||
/* MalformedBlock: MalformedOpenTag | MalformedCloseTag */
|
||||
@ -4448,10 +4464,12 @@ class SSTemplateParser extends Parser {
|
||||
$text = stripslashes($text);
|
||||
$text = addcslashes($text, '\'\\');
|
||||
|
||||
// TODO: This is pretty ugly & gets applied on all files not just html. I wonder if we can make this non-dynamically calculated
|
||||
// TODO: This is pretty ugly & gets applied on all files not just html. I wonder if we can make this
|
||||
// non-dynamically calculated
|
||||
$text = preg_replace(
|
||||
'/href\s*\=\s*\"\#/',
|
||||
'href="\' . (SSViewer::$options[\'rewriteHashlinks\'] ? strip_tags( $_SERVER[\'REQUEST_URI\'] ) : "") . \'#',
|
||||
'href="\' . (SSViewer::$options[\'rewriteHashlinks\'] ? strip_tags( $_SERVER[\'REQUEST_URI\'] ) : "") .
|
||||
\'#',
|
||||
$text
|
||||
);
|
||||
|
||||
@ -4467,10 +4485,10 @@ class SSTemplateParser extends Parser {
|
||||
*
|
||||
* @static
|
||||
* @throws SSTemplateParseException
|
||||
* @param $string - The source of the template
|
||||
* @param string $templateName - The name of the template, normally the filename the template source was loaded from
|
||||
* @param bool $includeDebuggingComments - True is debugging comments should be included in the output
|
||||
* @return mixed|string - The php that, when executed (via include or exec) will behave as per the template source
|
||||
* @param $string The source of the template
|
||||
* @param string $templateName The name of the template, normally the filename the template source was loaded from
|
||||
* @param bool $includeDebuggingComments True is debugging comments should be included in the output
|
||||
* @return mixed|string The php that, when executed (via include or exec) will behave as per the template source
|
||||
*/
|
||||
static function compileString($string, $templateName = "", $includeDebuggingComments=false) {
|
||||
if (!trim($string)) {
|
||||
@ -4481,7 +4499,8 @@ class SSTemplateParser extends Parser {
|
||||
$parser = new SSTemplateParser($string);
|
||||
$parser->includeDebuggingComments = $includeDebuggingComments;
|
||||
|
||||
// Ignore UTF8 BOM at begining of string. TODO: Confirm this is needed, make sure SSViewer handles UTF (and other encodings) properly
|
||||
// Ignore UTF8 BOM at begining of string. TODO: Confirm this is needed, make sure SSViewer handles UTF
|
||||
// (and other encodings) properly
|
||||
if(substr($string, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) $parser->pos = 3;
|
||||
|
||||
// Match the source against the parser
|
||||
@ -4494,12 +4513,14 @@ class SSTemplateParser extends Parser {
|
||||
|
||||
// Include top level debugging comments if desired
|
||||
if($includeDebuggingComments && $templateName && stripos($code, "<?xml") === false) {
|
||||
// If this template is a full HTML page, then put the comments just inside the HTML tag to prevent any IE glitches
|
||||
// If this template is a full HTML page, then put the comments just inside the HTML tag to prevent any IE
|
||||
// glitches
|
||||
if(stripos($code, "<html") !== false) {
|
||||
$code = preg_replace('/(<html[^>]*>)/i', "\\1<!-- template $templateName -->", $code);
|
||||
$code = preg_replace('/(<\/html[^>]*>)/i', "<!-- end template $templateName -->\\1", $code);
|
||||
} else {
|
||||
$code = str_replace('<?php' . PHP_EOL, '<?php' . PHP_EOL . '$val .= \'<!-- template ' . $templateName . ' -->\';' . "\n", $code);
|
||||
$code = str_replace('<?php' . PHP_EOL, '<?php' . PHP_EOL . '$val .= \'<!-- template ' . $templateName .
|
||||
' -->\';' . "\n", $code);
|
||||
$code .= "\n" . '$val .= \'<!-- end template ' . $templateName . ' -->\';';
|
||||
}
|
||||
}
|
||||
|
@ -662,8 +662,19 @@ class SSTemplateParser extends Parser {
|
||||
}
|
||||
|
||||
function NamedArgument_Value(&$res, $sub) {
|
||||
$res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php']
|
||||
: str_replace('$$FINAL', 'XML_val', $sub['php']);
|
||||
switch($sub['ArgumentMode']) {
|
||||
case 'string':
|
||||
$res['php'] .= $sub['php'];
|
||||
break;
|
||||
|
||||
case 'default':
|
||||
$res['php'] .= $sub['string_php'];
|
||||
break;
|
||||
|
||||
default:
|
||||
$res['php'] .= str_replace('$$FINAL', 'obj', $sub['php']) . '->self()';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*!*
|
||||
|
@ -96,7 +96,19 @@ class SSViewer_Scope {
|
||||
$this->upIndex, $this->currentIndex);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the current object and resets the scope.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function self() {
|
||||
$result = $this->itemIterator ? $this->itemIterator->current() : $this->item;
|
||||
$this->resetLocalScope();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function pushScope(){
|
||||
$newLocalIndex = count($this->itemStack)-1;
|
||||
|
||||
@ -821,7 +833,7 @@ class SSViewer {
|
||||
* @return string - The result of executing the template
|
||||
*/
|
||||
protected function includeGeneratedTemplate($cacheFile, $item, $overlay, $underlay) {
|
||||
if(isset($_GET['showtemplate']) && $_GET['showtemplate']) {
|
||||
if(isset($_GET['showtemplate']) && $_GET['showtemplate'] && Permission::check('ADMIN')) {
|
||||
$lines = file($cacheFile);
|
||||
echo "<h2>Template: $cacheFile</h2>";
|
||||
echo "<pre>";
|
||||
|
Loading…
x
Reference in New Issue
Block a user