Warning – Frontend Editing is a bit of work to install it the first time. :) But it’s worth the time.
1 Gentics Content.Node JS API
Find out more about the awesome API
2 Functionality in demo portal
These functionalities are implemented in the current demo example:
- Present EDIT and NEW link for editing pages or creating new pages
- Check editing permissions for this folder
- Display ID of page and folder name of page edited
- Use language code in case of creating a new page
- After publishing you have to clear the cache in the Gentics Content Connector.
In case you use FSCR, you can not use the instant publishing feature.
3 Configuration of Frontend Editing
3.1 How to make a tagtype editable in frontend (=portal)
If you use this implementation here, you can define each tagtype in the CMS templates which should be editable in frontend. This enables you to select which tagtypes to choose.
Example of editable tagtype, add id=“feedit_
3.2 CMS authentication module
You also have to configure the CMS authentication module in Gentics Portal.Node PHP.
<?php $this→widget(‘cmsuserauthentication.widgets.CmsuserauthenticationWidget’); ?>
// cms user authentication module 'cmsuserauthentication' => array( 'class' => 'common.modules.cmsuserauthentication.CmsuserauthenticationModule', 'authUrl' => 'http://cms.<domain of CMS>/CNPortletapp/rest/auth/login', //'http://'.$_SERVER['HTTP_HOST'].'/gcnproxy/CNPortletapp/rest/auth/login', 'salt_secretkey' => 'SecretSalt1234#', 'username_sessionattr' => 'email', 'cmsBackendUrl' => 'http://cms.<domain of CMS>/backend.php' ),
All CMS users which have to have access via frontend editing have to get a password md5-encrypted out of salt-secretkey and username. Code: $password = md5($this→salt_secretkey.$username);
Place this script on your CMS server in /Node/apache/htdocs/ named backend.php
<?php /** * Get cookie value and sid, decrypt and save cookie, forward user * to cms backend with given sid */ // get 2 values $value = $_GET['value']; $sid = $_GET['sid']; if (isset($sid) && isset($value)) { // include encryption class include_once 'encryption_class.php'; $crypt = new encryption_class(); // encryption key must match the key in the portal config $key = "secretKey123#"; $min_length = 8; $value = urldecode($crypt->decrypt($key, $value)); //set cookie setcookie ("GCN_SESSION_SECRET", $value, time()+60*60*24, '/'); //forward to cms echo '<meta http-equiv="refresh" content='; echo '"0; URL=/.Node/?sid='.$sid.'/">'; } ?>
Code of encryption_class.php
<?php // ****************************************************************************** // A reversible password encryption routine by: // Copyright 2003-2009 by A J Marston <http://www.tonymarston.net> // Distributed under the GNU General Public Licence // Modification: May 2007, M. Kolar <http://mkolar.org>: // No need for repeating the first character of scramble strings at the end; // instead using the exact inverse function transforming $num2 to $num1. // Modification: Jan 2009, A J Marston <http://www.tonymarston.net>: // Use mb_substr() if it is available (for multibyte characters). // ****************************************************************************** class encryption_class { var $scramble1; // 1st string of ASCII characters var $scramble2; // 2nd string of ASCII characters var $errors; // array of error messages var $adj; // 1st adjustment value (optional) var $mod; // 2nd adjustment value (optional) // **************************************************************************** // class constructor // **************************************************************************** function encryption_class () { $this->errors = array(); // Each of these two strings must contain the same characters, but in a different order. // Use only printable characters from the ASCII table. // Do not use single quote, double quote or backslash as these have special meanings in PHP. // Each character can only appear once in each string. $this->scramble1 = '! #$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'; $this->scramble2 = 'f^jAE]okIOzU[2&q1{3`h5w_794p@6s8?BgP>dFV=m D<TcS%Ze|r:lGK/uCy.Jx)HiQ!#$~(;Lt-R}Ma,NvW+Ynb*0X'; if (strlen($this->scramble1) <> strlen($this->scramble2)) { trigger_error('** SCRAMBLE1 is not same length as SCRAMBLE2 **', E_USER_ERROR); } // if $this->adj = 1.75; // this value is added to the rolling fudgefactors $this->mod = 3; // if divisible by this the adjustment is made negative } // constructor // **************************************************************************** function decrypt ($key, $source) // decrypt string into its original form { $this->errors = array(); // convert $key into a sequence of numbers $fudgefactor = $this->_convertKey($key); if ($this->errors) return; if (empty($source)) { $this->errors[] = 'No value has been supplied for decryption'; return; } // if $target = null; $factor2 = 0; for ($i = 0; $i < strlen($source); $i++) { // extract a (multibyte) character from $source if (function_exists('mb_substr')) { $char2 = mb_substr($source, $i, 1); } else { $char2 = substr($source, $i, 1); } // if // identify its position in $scramble2 $num2 = strpos($this->scramble2, $char2); if ($num2 === false) { $this->errors[] = "Source string contains an invalid character ($char2)"; return; } // if // get an adjustment value using $fudgefactor $adj = $this->_applyFudgeFactor($fudgefactor); $factor1 = $factor2 + $adj; // accumulate in $factor1 $num1 = $num2 - round($factor1); // generate offset for $scramble1 $num1 = $this->_checkRange($num1); // check range $factor2 = $factor1 + $num2; // accumulate in $factor2 // extract (multibyte) character from $scramble1 if (function_exists('mb_substr')) { $char1 = mb_substr($this->scramble1, $num1, 1); } else { $char1 = substr($this->scramble1, $num1, 1); } // if // append to $target string $target .= $char1; //echo "char1=$char1, num1=$num1, adj= $adj, factor1= $factor1, num2=$num2, char2=$char2, factor2= $factor2<br />\n"; } // for return rtrim($target); } // decrypt // **************************************************************************** function encrypt ($key, $source, $sourcelen = 0) // encrypt string into a garbled form { $this->errors = array(); // convert $key into a sequence of numbers $fudgefactor = $this->_convertKey($key); if ($this->errors) return; if (empty($source)) { $this->errors[] = 'No value has been supplied for encryption'; return; } // if // pad $source with spaces up to $sourcelen $source = str_pad($source, $sourcelen); $target = null; $factor2 = 0; for ($i = 0; $i < strlen($source); $i++) { // extract a (multibyte) character from $source if (function_exists('mb_substr')) { $char1 = mb_substr($source, $i, 1); } else { $char1 = substr($source, $i, 1); } // if // identify its position in $scramble1 $num1 = strpos($this->scramble1, $char1); if ($num1 === false) { $this->errors[] = "Source string contains an invalid character ($char1)"; return; } // if // get an adjustment value using $fudgefactor $adj = $this->_applyFudgeFactor($fudgefactor); $factor1 = $factor2 + $adj; // accumulate in $factor1 $num2 = round($factor1) + $num1; // generate offset for $scramble2 $num2 = $this->_checkRange($num2); // check range $factor2 = $factor1 + $num2; // accumulate in $factor2 // extract (multibyte) character from $scramble2 if (function_exists('mb_substr')) { $char2 = mb_substr($this->scramble2, $num2, 1); } else { $char2 = substr($this->scramble2, $num2, 1); } // if // append to $target string $target .= $char2; //echo "char1=$char1, num1=$num1, adj= $adj, factor1= $factor1, num2=$num2, char2=$char2, factor2= $factor2<br />\n"; } // for return $target; } // encrypt // **************************************************************************** function getAdjustment () // return the adjustment value { return $this->adj; } // setAdjustment // **************************************************************************** function getModulus () // return the modulus value { return $this->mod; } // setModulus // **************************************************************************** function setAdjustment ($adj) // set the adjustment value { $this->adj = (float)$adj; } // setAdjustment // **************************************************************************** function setModulus ($mod) // set the modulus value { $this->mod = (int)abs($mod); // must be a positive whole number } // setModulus // **************************************************************************** // private methods // **************************************************************************** function _applyFudgeFactor (&$fudgefactor) // return an adjustment value based on the contents of $fudgefactor // NOTE: $fudgefactor is passed by reference so that it can be modified { $fudge = array_shift($fudgefactor); // extract 1st number from array $fudge = $fudge + $this->adj; // add in adjustment value $fudgefactor[] = $fudge; // put it back at end of array if (!empty($this->mod)) { // if modifier has been supplied if ($fudge % $this->mod == 0) { // if it is divisible by modifier $fudge = $fudge * -1; // make it negative } // if } // if return $fudge; } // _applyFudgeFactor // **************************************************************************** function _checkRange ($num) // check that $num points to an entry in $this->scramble1 { $num = round($num); // round up to nearest whole number $limit = strlen($this->scramble1); while ($num >= $limit) { $num = $num - $limit; // value too high, so reduce it } // while while ($num < 0) { $num = $num + $limit; // value too low, so increase it } // while return $num; } // _checkRange // **************************************************************************** function _convertKey ($key) // convert $key into an array of numbers { if (empty($key)) { $this->errors[] = 'No value has been supplied for the encryption key'; return; } // if $array[] = strlen($key); // first entry in array is length of $key $tot = 0; for ($i = 0; $i < strlen($key); $i++) { // extract a (multibyte) character from $key if (function_exists('mb_substr')) { $char = mb_substr($key, $i, 1); } else { $char = substr($key, $i, 1); } // if // identify its position in $scramble1 $num = strpos($this->scramble1, $char); if ($num === false) { $this->errors[] = "Key contains an invalid character ($char)"; return; } // if $array[] = $num; // store in output array $tot = $tot + $num; // accumulate total for later } // for $array[] = $tot; // insert total as last entry in array return $array; } // _convertKey // **************************************************************************** } // end encryption_class // **************************************************************************** ?>
3.3 CMS content element for Frontend Editing
This code can be used in a content element (tagtype) for including frontend editing in content pages.
It is absolutely important to set the right version of the CMS. Open this URL to find our the current version. http://cms.
#if($cms.rendermode.publish)## <?php $page['id'] = '<node page.id>'; $page['language'] = '<node page.language.code>'; $page['template'] = '<node page.template.id>'; // only if user is logged in, frontend editing is possible if (Yii::app()->user->email != '') { ?> <script type="text/javascript" src="<node feedit>"></script> <?php if (isset( $_GET['edit']) || isset( $_GET['new']) ) {?> <style> .frontendpanel { left: 0px; position: absolute; text-align:left; } body { padding-top:0px !important; } </style> #set($cms_version = "<node cmsversion>")## <script type="text/javascript"> /**$(document).ready(function() {**/ Aloha = {}; Aloha.settings = { "ribbon" : "1", "contentHandler" : { "initEditable" : ["blockelement"], "getContents": ["blockelement", "basic"] }, "plugins" : { gcn: { "buildRootTimestamp" : "$!{cms_version}", "webappPrefix" : "/gcnproxy/CNPortletapp/", "stag_prefix" : "/gcnproxy/.Node/", "id" : "<?php echo $page['id']; ?>", //set pageid here "links": "frontend", "nodeFolderId" : "$cms.node.folder.id", "nodeId" : "$cms.node.id", "editables" : { ".title,.subtitle,.teaser" : { "tagtypeWhitelist" : [] } } }, "table" : { "config" : ["table"], "tableConfig" : [ { "name" : "variation1" }, { "name" : "variation2" } ] } }, "toolbar" : { "tabs" : [ { "label" : "tab.format.label" }, { "label" : "tab.insert.label", "components" : [ [ "gcnArena" ] ] }, { "label" : "tab.link.label", "components" : [ "editLink", "removeLink", "linkBrowser", "gcnLinkBrowser" ] }] } }; /** }); **/ </script> <!-- Load Aloha Editor --> <script src="/gcnproxy/CNPortletapp/$!{cms_version}/alohaeditor/lib/aloha.js" data-aloha-plugins="common/ui, common/block, extra/ribbon, common/format, common/list, common/link, common/table, common/paste, common/contenthandler, common/commands, gcn/gcn-linkbrowser, gcn/gcn, common/characterpicker, common/horizontalruler"></script> <!-- Load Aloha Editor CSS --> <link rel="stylesheet" href="/gcnproxy/CNPortletapp/$!{cms_version}/alohaeditor/css/aloha.css"></script> <script type="text/javascript"> (function (window) { // use Aloha Editor's internal jQuery & expose it var Aloha = window.Aloha; //window.$ = window.jQuery = window.Aloha.jQuery; Aloha.ready(function(){ GCN = window.GCN; GCN.settings.BACKEND_PATH="/gcnproxy/CNPortletapp"; var sid = '<?php $this->widget('cmsuserauthentication.widgets.CmsuserauthenticationWidget'); ?>'; if(sid==='0' || sid===''){ alert("Error: Authentication on CMS failed"); } else{ GCN.setSid(sid); Aloha.settings.plugins.gcn.sid = GCN.sid; //GCN.login('node', 'node', function(success, data) { <?php if(isset($_GET['edit'])){ ?> <?php if(isset($_GET['pageid'])){ ?> gtx_feEditor.init({language: '<?php if ( isset( $page['language'] ) ) echo $page['language']; else echo 'en'; ?>', id: '<?php echo $_GET['pageid']; ?>'}); <?php } else {?> gtx_feEditor.init({language: '<?php if ( isset( $page['language'] ) ) echo $page['language']; else echo 'en'; ?>', id: '<?php if ( isset( $page['id'] ) ) echo $page['id']; else echo 'unknown'; ?>'}); <?php } ?> <?php } else{ ?> gtx_feEditor.createPage({language: '<?php if ( isset( $page['language'] ) ) echo $page['language']; else echo 'en'; ?>', id: '<?php if ( isset( $page['id'] ) ) echo $page['id']; else echo 'unknown'; ?>'}); <?php } ?> //}); } }); }(window)); </script> <style> .aloha-editable { min-height: 1.2em; outline: #FFD600 dotted 3px; } </style> <?php } } ?> #end##
3.4 This is the code for FEEdit.js
var gtx_feEditor = function () { // function for reading get parameters function getParam(variable) { //filter parameters from url var query = window.location.search.substring(1); var vars = query.split("&"); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split("="); if (pair[0] == variable) { return pair[1]; } } return (false); } return { id: null, language: 'de', i18n: null, init: function (data) { var that = this; //this.id = Aloha.settings.plugins.gcn.id; //set id to the current pageId if(data.language !== ''){ this.language = data.language; } this.id = data.id; this.i18n = new Object(); this.i18n['en'] = new Object({ // english version of panel labels newpage: "new", edit: "edit", save: "save only", publish: "publish", cancel: "cancel", pageid: "PageID", folderid: "Folder", locked: "the page is currently locked by an other user" }); this.i18n['de'] = new Object({ // german version of panel labels newpage: "neu", edit: "bearbeiten", save: "nur speichern", publish: "veröffentlichen", cancel: "abbrechen", pageid: "SeitenID", folderid: "Ordner", locked: "Die Seite ist von einem anderen Benutzer gesperrt, deshalb wurde sie im Lesemodus geƶffnet." }); if (getParam('edit') == 'true') { GCN.page(this.id, function (page) { that._createUserPanel(true); that._renderTagsInEditMode(true, that); that._setClickHandlers(); }); } else if (getParam('new') == 'true') { GCN.page(this.id, function (page) { that._createUserPanel(true); that._renderTagsInEditMode(true, that) that._setClickHandlers(); }); } else { this._createUserPanel(false); } }, save: function () { GCN.page(this.id).save(); //save page }, publish: function () { var that = this; GCN.page(this.id).save(function(page){ page.publish(function(){ window.location = window.location.pathname; }); }); }, cancel: function () { var that = this; GCN.page(this.id).unlock(function(){ window.location = window.location.pathname; }); }, // create new UserPanel if it doesn't exist or update the existing one _createUserPanel: function (inEditmode) { //inEditmode is used to decide if panel should show edit/new or publish,save,cancel var that = this; var i18n = that.i18n[that.language]; var html = ''; if (inEditmode) { var folder = GCN.page(this.id).prop('folder'); html += '<span class="frontendpanel"><span class="pageid">' + i18n.pageid + ': ' + this.id + ' </span> | '; html += '<a class="metanavlink" data-action="publish" href="#">' + i18n.publish + '</a> | '; html += '<a class="metanavlink" data-action="save" href="#">' + i18n.save + '</a> | '; html += '<a class="metanavlink" data-action="cancel" href="#">' + i18n.cancel + '</a> | '; html += '<span class="folderid" data-folderid="' + folder.id + '"> ' + i18n.folderid + ': ' + folder.name + '</span></span>'; } if ($('div.metanav').length > 0) { if ($('div.metanav span.frontendpanel').length > 0) { $('div.metanav span.frontendpanel').remove(); $('div.metanav').prepend(html); } else { $('div.metanav').prepend(html); } } else { $('body').prepend('<div class="metanav">' + html + '</div><div class="space"></div>'); } }, _setClickHandlers: function () { var that = this; $('.frontendpanel a.metanavlink').click(function () { switch ($(this).attr('data-action')) { case 'publish': that.publish(); break; case 'save': that.save(); break; case 'cancel': that.cancel(); break; default: break; } }); }, createPage: function (data) { //creates a new page in the same directory and with the same template as the current page var that = this; GCN.page(data.id, function (page) { var templateid = page.prop('templateId'); //get templateId var folderid = page.prop('folderId'); //get folderId GCN.folder(folderid).createPage(templateid, { language: data.language }, function (newpage) { //create new page in current folder with the current template var url = window.location.href.substring(0, top.location.href.indexOf('?')); url += '?edit=true&pageid='+newpage.id(); window.location= url; }); }); }, _renderTagsInEditMode: function (edit, that) { //renders all tags that are defined for frontendediting (wrapping div with data-edit=true and tagname in editmode GCN.page(this.id, function (page) { if(edit===true){ if(page.prop('readOnly')===true){ that._renderTagsInEditMode(false, that); that._createUserPanel(false); } } $('span[data-edit=true]').each(function () { //get all spans that are supposed to be frontendeditable var that = this; //set that to this, so it can be accessed in tag success function page.tag($(that).attr('data-tagname'), function (tag) { //get tagname from data attribute if (edit == true)tag.edit($(that)); //render tag in editmode else{ tag.render($(that)); } }); }); $('div[data-edit=true]').each(function () { //get all divs that are supposed to be frontendeditable var that = this; //set that to this, so it can be accessed in tag success function page.tag($(that).attr('data-tagname'), function (tag) { //get tagname from data attribute if (edit == true) tag.edit($(that)); //render tag in editmode else{ tag.render($(that));//render tag in previewmode } }); }); if(edit != true){ for (var i = Aloha.editables.length-1; i>=0; i--) Aloha.editables[i].destroy(); } }); } }; }();
4 HTTP Proxy
4.1 Aloha Editor – Frontend Editing
To request the Aloha Editor libraries from the CMS server (with probably a different domain) via Ajax, you have to use this proxy, so your requests look like local portal requests, i.e. http://www.
Additionally you need the proxy on the CMS server for requesting the previews of modules as local requests on the CMS server.
Create a directory in
4.2 Proxy files
We added a HTTP proxy to the examples, but we can not garantuee 100% functionality. This proxy did a good job for us, but it’s not our product.
Also think about the security issues. A proxy is also a back-door to a backend system, in case your CMS is inside your firewall, you open a door for a potential intruder. So please use it with care and ask your system administrators for advice to use it for good.
Find this example in scripts/examples
.htaccess – redirects all path after gcnproxy to proxy … /gcnproxy/.Node/?do=100 index.php – proxy script http.inc.php – part of proxy script settings.conf.php – settings file of proxy script
4.3 .htaccess
RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php [QSA,L] # This is a hack for PHP to prevent it from parsing multipart/form-data # There is no other way to do this, but if you know a better way feel free to change this. # This basically rewrites the Content-Type if it's multipart/form-data and saves it in another header. SetEnvIf Content-Type ^(multipart/form-data)(?!-proxyphp)(.*) NEW_CONTENT_TYPE=multipart/form-data-proxyphp$2 OLD_CONTENT_TYPE=$1$2 RequestHeader set Content-Type %{NEW_CONTENT_TYPE}e env=NEW_CONTENT_TYPE RequestHeader set X-proxyphp-Content-Type %{OLD_CONTENT_TYPE}e env=NEW_CONTENT_TYPE
4.4 settings.conf.php
<?php // The URL of the backend server that browser requests should be proxied to $CMS_SERVERHOST = 'http://cms.gportal-dev.gentics.com/'; // The path to the proxy script on the frontend server if ( !isset( $PROXYNAME ) ) $PROXYNAME = '/gcnproxy/'; // This is needed for Gentics Content.Node to set the URL's to this proxy. // You can add multiple parameters by just appending them with a & $HTTP_URL_ADD_QUERY_PARAMETERS = 'proxyprefix=' . $PROXYNAME; // Max times to follow a HTTP redirection response to a new URL $HTTP_MAX_REDIRECTS = 10;
4.5 http.inc.php – part of gcnproxy
<?php /** * Query an HTTP(S) URL with the given request parameters and return the * response headers and status code. The socket is returned as well and * will point to the begining of the response payload (after all headers * have been read), and must be closed with fclose(). * @param $url the request URL * @param $request the request method may optionally be overridden. * @param $timeout connection and read timeout in seconds */ function http_request($request, $timeout = 5) { $url = $request['url']; // Extract the hostname from url $parts = parse_url($url); if (array_key_exists('host', $parts)) { $remote = $parts['host']; } else { return myErrorHandler("url ($url) has no host. Is it relative?"); } if (array_key_exists('port', $parts)) { $port = $parts['port']; } else { $port = 0; } // Beware that RFC2616 (HTTP/1.1) defines header fields as case-insensitive entities. $request_headers = ""; foreach ($request['headers'] as $name => $value) { switch (strtolower($name)) { //omit some headers case "keep-alive": case "connection": //TODO: we don't handle any compression encodings. compression //can cause a problem if client communication is already being //compressed by the server/app that integrates this script //(which would double compress the content, once from the remote //server to us, and once from us to the client, but the client //would de-compress only once). case "accept-encoding": break; // correct the host parameter case "host": $host_info = $remote; if ($port) { $host_info .= ':' . $port; } $request_headers .= "$name: $host_info\r\n"; break; // forward all other headers default: $request_headers .= "$name: $value\r\n"; break; } } //set fsockopen transport scheme, and the default port switch (strtolower($parts['scheme'])) { case 'https': $scheme = 'ssl://'; if ( ! $port ) $port = 443; break; case 'http': $scheme = ''; if ( ! $port ) $port = 80; break; default: //some other transports are available but not really supported //by this script: http://php.net/manual/en/transports.inet.php $scheme = $parts['scheme'] . '://'; if ( ! $port ) { return myErrorHandler("Unknown scheme ($scheme) and no port."); } break; } //we make the request with socket operations since we don't want to //depend on the curl extension, and the higher level wrappers don't //give us usable error information. $sock = @fsockopen("$scheme$remote", $port, $errno, $errstr, $timeout); if ( ! $sock ) { return myErrorHandler("Unable to open URL ($url): $errstr"); } //the timeout in fsockopen is only for the connection, the following //is for reading the content stream_set_timeout($sock, $timeout); //an absolute url should only be specified for proxy requests if (array_key_exists('path', $parts)) { $path_info = $parts['path']; } else { $path_info = '/'; } if (array_key_exists('query', $parts)) $path_info .= '?' . $parts['query']; if (array_key_exists('fragment', $parts)) $path_info .= '#' . $parts['fragment']; $out = $request["method"]." ".$path_info." ".$request["protocol"]."\r\n" . $request_headers . "Connection: close\r\n\r\n"; fwrite($sock, $out); fwrite($sock, $request['payload']); $header_str = stream_get_line($sock, 1024*16, "\r\n\r\n"); $headers = http_parse_headers($header_str); $status_line = array_shift($headers); // get http status preg_match('|HTTP/\d+\.\d+\s+(\d+)\s+.*|i',$status_line,$match); $status = ''; if (isset($match[1])) { $status=$match[1]; } return array('headers' => $headers, 'socket' => $sock, 'status' => $status); } /** * Parses a string containing multiple HTTP header lines into an array * of key => values. * Inspired by HTTP::Daemon (CPAN). */ function http_parse_headers($header_str) { $headers = array(); //ignore leading blank lines $header_str = preg_replace("/^(?:\x0D?\x0A)+/", '', $header_str); while (preg_match("/^([^\x0A]*?)\x0D?(?:\x0A|\$)/", $header_str, $matches)) { $header_str = substr($header_str, strlen($matches[0])); $status_line = $matches[1]; if (empty($headers)) { // the status line $headers[] = $status_line; } elseif (preg_match('/^([^:\s]+)\s*:\s*(.*)/', $status_line, $matches)) { if (isset($key)) { //previous header is finished (was potentially multi-line) $headers[$key] = $val; } list(,$key,$val) = $matches; } elseif (preg_match('/^\s+(.*)/', $status_line, $matches)) { //continue a multi-line header $val .= " ".$matches[1]; } else { //empty (possibly malformed) header signals the end of all headers break; } } if (isset($key)) { $headers[$key] = $val; } return $headers; }
4.6 index.php – the main proxy script
<?php /** * Testing from the command line: * function getallheaders(){return array('X-Gentics' => 'X');}; * https url example: https://google.com/adsense * */ require_once "settings.conf.php"; require_once "http.inc.php"; $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.0'; $request = array( 'method' => $_SERVER['REQUEST_METHOD'], 'protocol' => $_SERVER['SERVER_PROTOCOL'], 'headers' => getallheaders(), // multipart/form-data should work when the directives // in the .htaccess are working to prevent PHP from parsing // our data. This is done by a hack 'payload' => file_get_contents('php://input'), ); // Check if the header X-proxyphp-Content-Type was set by our mod-rewrite rule. // If yes: restore the original Content-Type header. if (isset($request['headers']['X-proxyphp-Content-Type'])) { $request['headers']['Content-Type'] = $request['headers']['X-proxyphp-Content-Type']; unset($request['headers']['X-proxyphp-Content-Type']); } $url = $_SERVER['REQUEST_URI']; // use URL from GET-parameters if ( $reset_url == true ) $url = $_GET['url']; if (strpos($url, $PROXYNAME) === 0) { $url = substr($url, strlen($PROXYNAME)); } // Make sure that the URL starts with a / ofr security reasons. if ($url[0] !== '/') { $url = '/' . $url; } // Unset some headers which shouldn't get forwarded if (isset($request['headers']['If-None-Match'])){ unset($request['headers']['If-None-Match']); } if (isset($request['headers']['If-Modified-Since'])){ unset($request['headers']['If-Modified-Since']); } if (isset($request['headers']['If-Modified-Since'])){ unset($request['headers']['If-Modified-Since']); } // Add parameters to the query URL if specified if (!empty($HTTP_URL_ADD_QUERY_PARAMETERS) && strpos($url, '?') === false){ $url = $url . '?' . $HTTP_URL_ADD_QUERY_PARAMETERS; } else { $url = $url . '&' . $HTTP_URL_ADD_QUERY_PARAMETERS; } // Remove slash at the end of $CMS_SERVERHOST if there is one if (substr($url, -1) === '/') { $CMS_SERVERHOST = substr($CMS_SERVERHOST, 0, -1); } $request['url'] = $CMS_SERVERHOST . $url; $response = http_request($request); // Note HEAD does not always work even if specified... // We use HEAD for Linkchecking so we do a 2nd request. if ($_SERVER['REQUEST_METHOD'] === 'HEAD' && (int)$response['status'] >= 400) { $request['method'] = 'GET'; fpassthru($response['socket']); fclose($response['socket']); $response = http_request($request); } else { $n_redirects = 0; // Follow redirections until we got a page finally, but only max $HTTP_MAX_REDIRECTS times. while (in_array($response['status'], array(301, 302, 307))) { // We got a redirection request from the remote server, // let's follow the trail and see what happens... // We don't check if the URL is an external now, because the response // should be trustworthy. // We handle HTTP 301, 302 and 307 // See: http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection // Close the old socket fclose($response['socket']); if ($n_redirects++ == $HTTP_MAX_REDIRECTS) { myErrorHandler('Too many redirects (' . $n_redirects . '), exiting...'); } if (empty($response['headers']['Location'])) { myErrorHandler("Got redirection request by the remote server, but the redirection URL is empty."); } $n_redirects++; $url = $response['headers']['Location']; // For some unknown reason we got the new URL with the proxyname // prepended back, so we have to remove that part of the URL. // parse_url: http://php.net/manual/de/function.parse-url.php $parsedURL = parse_url($url); $url_path = $parsedURL['path']; // Check if the redirection URL starts with our proxyname if (substr($url_path, 0, strlen($PROXYNAME)) === $PROXYNAME) { $url_path = substr($url_path, strlen($PROXYNAME)); // Now build the new URL $url = $parsedURL['scheme'] . '://' . $parsedURL['host'] . $url_path . (empty($parsedURL['query']) ? '' : '?' . $parsedURL['query']); } $request['url'] = $url; $response = http_request($request); } } // Forward the response code to our client // this sets the response code. // we don't use http_response_code() as that only works for PHP >= 5.4 header('HTTP/1.0 ' . $response['status']); // forward each returned header... foreach ($response['headers'] as $key => $value) { if (strtolower($key) == 'content-length') { // There is no need to specify a content length since we don't do keep // alive, and this can cause problems for integration (e.g. gzip output, // which would change the content length) // Note: overriding with header('Content-length:') will set // the content-length to zero for some reason continue; } header($key . ': ' . $value); } header('Connection: close'); // output the contents if any if (null !== $response['socket']) { fpassthru($response['socket']); fclose($response['socket']); } function myErrorHandler($msg) { // 500 could be misleading... // Should we return a special Error when a proxy error occurs? header("HTTP/1.0 500 Internal Error"); die("AJAX Gateway Error: $msg"); }