1<?php 2/** 3 * Handler performs request handling and file proxying via stepping in front of the 404 handler 4 * 5 * @package Weebly 6 * @subpackage ResellerServices 7 * @author Dustin Doiron <[email protected]> 8 * @since 2014-07-01 9 * @copyright 2014 Weebly, Inc 10 */ 11require_once( __DIR__ . '/Bootstrap.php' ); 12 13define("MEMORY_LIMIT", 1048576); 14 15class Handler 16{ 17 /** 18 * @var $request 19 */ 20 private $request = NULL; 21 22 /** 23 * @var $site 24 */ 25 private $site = NULL; 26 27 /** 28 * @var boolean 29 */ 30 private $isRedirect = false; 31 32 /** 33 * Constructor 34 * On construct, Handler runs through everything needed to either render the requested page, or render a 404 35 * No methods are called outside of this class, and no values are returned 36 * 37 * @param array $request 38 * 39 * @return void 40 */ 41 public function __construct( $request ) 42 { 43 $this->buildSiteArray( ); 44 $this->buildRequestArray( $request ); 45 46 /** 47 * Is this an API request to a client API that we need to proxy along? 48 */ 49 if ( $this->isClientApiRequest( ) === true ) 50 { 51 \OriginAPI::makeClientAPIRequest( $this->request, file_get_contents( 'php://input' ) ); 52 } 53 54 if ( $this->isPage( ) === true ) 55 { 56 /** 57 * Might have the mobile cookie, and just need to get redirected to mobile file on disk 58 * For dynamic mobile page, always let dynamic page handle it. 59 */ 60 if ( 61 $this->request['mobile'] === true && 62 file_exists( \BASE_DOCROOT_DIR . '/mobile/' . $this->request['file'] ) === true 63 ) { 64 \setcookie( 'is_mobile', 1, time( ) + 2592000, '/' ); 65 if ($this->isDynamicPage() === false) { 66 \Output::sendHeader('Location: ' . $this->request['file']); 67 $this->isRedirect = true; 68 $this->finalizeOutput(); 69 exit(); 70 } 71 } 72 73 /** 74 * Do we have it in the page hierarchy, or is it a dynamic page? Go get it from Origin 75 */ 76 if ($this->isPageInPublishedData( $this->request['file'] ) === true || $this->isDynamicPage( ) === true ) 77 { 78 $this->isDynamicStandardPage(); // update request with isDynamic if needed 79 $response = \OriginRequest::getObject( $this->request ); 80 $this->handleOriginResponse( $response ); 81 } else { 82 \Output::render404( ); 83 } 84 85 /** 86 * Should we try a simple redirect from .htm to .html? 87 */ 88 if ( $this->isPageInPublishedData( $this->request['file'] . 'l' ) === true ) 89 { 90 if ( file_exists( \BASE_DOCROOT_DIR . '/' . $this->request['file'] ) === true ) 91 { 92 \Output::sendHeader( 'Location: ' . $this->request['file'] . 'l' ); 93 $this->isRedirect = true; 94 $this->finalizeOutput(); 95 exit( ); 96 } 97 else 98 { 99 /** 100 * Don't have it yet, go get it before forwarding 101 */ 102 $this->request['file'] .= 'l'; 103 $this->isDynamicStandardPage(); // update request with isDynamic if needed 104 $response = \OriginRequest::getObject( $this->request ); 105 $this->handleOriginResponse( $response ); 106 } 107 } 108 } 109 else 110 { 111 /** 112 * Not a page, we have to use benefit of the doubt here for checking origin 113 */ 114 // Assets files. 115 // Set default memory_limit so wServer will send back retryRaw message for file larger than 1MB. 116 // Not setting it means it's remote server published before this change, 117 // then wServer will always return old type of response. (no retryRaw mechanism) 118 $this->request['memory_limit'] = MEMORY_LIMIT; 119 $response = \OriginRequest::getObject( $this->request ); 120 $this->handleOriginResponse( $response ); 121 } 122 123 // if redirect header, then add text to prevent some ftp server's firewall from block pure header redirect. 124 $this->finalizeOutput(); 125 } 126 127 /** 128 * Builds the site array for the current request 129 * 130 * @return void 131 */ 132 private function buildSiteArray( ) 133 { 134 if ( file_exists( \BASE_SERVICES_DIR . '/' . Configuration::PUBLISHED_DATA_LOCATION ) === true ) 135 { 136 $this->site = json_decode( file_get_contents( \BASE_SERVICES_DIR . '/' . Configuration::PUBLISHED_DATA_LOCATION ), true ); 137 } 138 } 139 140 /** 141 * Builds the request data array for the current request 142 * During one of the build cases, we may redirect out to the properly formed .html location 143 * 144 * @param array $request 145 * 146 * @return void 147 */ 148 private function buildRequestArray( $request ) 149 { 150 // Better detection of HTTPS 151 if (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == '1')) { 152 $_SERVER['REQUEST_SCHEME'] = 'https'; 153 } elseif (!isset($_SERVER['REQUEST_SCHEME'])) { 154 $_SERVER['REQUEST_SCHEME'] = 'http'; 155 } 156 157 $this->request = parse_url( $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'] ); 158 159 // Always trim the trailing slash. 160 $this->request['path'] = rtrim($this->request['path'], '/'); 161 $this->request['directories'] = explode('/', trim( $this->request['path'], '/' )); 162 $this->request['headers'] = self::getHeaders( ); 163 $this->request['method'] = $_SERVER['REQUEST_METHOD']; 164 165 if ( count( $this->request['directories'] ) > 1 ) 166 { 167 $this->request['file'] = array_pop( $this->request['directories'] ); 168 } 169 else 170 { 171 unset( $this->request['directories'] ); 172 $this->request['file'] = trim($this->request['path'], '/'); 173 } 174 175 if ( strpos( $this->request['file'], '.' ) === false ) 176 { 177 /** 178 * Is this a redirect to a .html file? 179 */ 180 $file = trim($this->request['path'], '/' ) . '.html'; 181 182 if ( $this->isPageInPublishedData( $file ) === true ) 183 { 184 \Output::sendHeader( 'Location: /' . $file ); 185 $this->isRedirect = true; 186 $this->finalizeOutput(); 187 exit( ); 188 } 189 190 if ( $this->request['file'] !== '' ) 191 { 192 $this->request['directories'][] = $this->request['file']; 193 } 194 195 $this->request['file'] = 'index.html'; 196 197 if ( $this->request['path'] === '/' ) 198 { 199 $this->request['path'] = $this->request['path'] . $this->request['file']; 200 } 201 } 202 203 $this->request['ua'] = $_SERVER['HTTP_USER_AGENT']; 204 $this->request['mobile'] = ( ( isset( $_COOKIE['disable_mobile'] ) === false || $_COOKIE['disable_mobile'] === '0' ) && 205 ( isset( $_COOKIE['is_mobile'] ) && $_COOKIE['is_mobile'] !== '0' || isset( $this->request['directories'] ) && $this->request['directories'][0] === 'mobile' ) ); 206 } 207 208 /** 209 * Handles a response from Origin, generally the last item in the lifecycle of a request 210 * 211 * @param mixed $response 212 * 213 * @return void 214 */ 215 private function handleOriginResponse( $response ) 216 { 217 /** 218 * Origin didn't have the object 219 */ 220 if ( $response === false ) 221 { 222 \Output::render404( ); 223 } 224 225 /** 226 * We're good to go, start rendering 227 */ 228 \Output::sendHeader( $_SERVER['SERVER_PROTOCOL'] . ' 200 OK' ); 229 230 /** 231 * Origin had the object, and it's now stored on disk, render the stored object 232 */ 233 if ( $response === true ) 234 { 235 if ( isset( $_COOKIE['is_redirecting'] ) === true ) 236 { 237 sleep( 2 ); 238 } 239 240 \setcookie( 'is_redirecting', 1, time( ) + 5 ); 241 \Output::sendHeader( 'Location: ' . $this->request['path'] ); 242 $this->isRedirect = true; 243 } 244 245 if ( is_object( $response ) === true ) 246 { 247 /** 248 * It's a streaming object, so we'll render it from here 249 */ 250 \Output::sendHeader( $_SERVER['SERVER_PROTOCOL'] . ' 200 OK' ); 251 252 if ($response->type && $response->type === 'js') { 253 \Output::sendHeader('Content-Type: text/javascript;'); 254 } 255 256 \Output::render( \Output::decodeWireObject( $response->object ) ); 257 } 258 } 259
260 /** 261 * Determines if the current request is to a page 262 * 263 * @return bool 264 */ 265 private function isPage( ) 266 { 267 /** 268 * The only pages with directories are mobile pages and dynamic pages (commerce & blog) 269 */ 270 if( isset( $this->request['directories'] ) === true 271 && count( $this->request['directories'] ) > 0 272 && $this->request['directories'] !== 'mobile' && $this->isDynamicPage( ) === false 273 ) 274 { 275 $this->request['isPage'] = false; 276 return false; 277 } 278 279 if ( preg_match( '/.html\Z/', $this->request['file'] ) > 0 || preg_match( '/.htm\Z/', $this->request['file'] ) > 0 ) 280 { 281 $this->request['isPage'] = true; 282 return true; 283 } 284 285 $this->request['isPage'] = false; 286 return false; 287 } 288 289 /** 290 * Uses base directory to determine if the a page is a dynamic page (blog & commerce) 291 * These pages are not always in published data and therefore require directory checking 292 * 293 * @return bool 294 */ 295 private function isDynamicPage() 296 { 297 if ( isset( $this->request['directories'] ) === true && count( $this->request['directories'] ) > 0 ) 298 { 299 /** 300 * Check if either the base directory is a store or a call to a file in the apps folder 301 * or if it is the base directory for a blog (meaning the base directory is also a file in published data) 302 */ 303 if ( $this->isDynamicRoute( $this->request['directories'][0] ) || 304 ( is_numeric( $this->request['directories'][0] ) === true ) 305 ) 306 { 307 return true; 308 } 309 } 310 311 return false; 312 } 313 314 /** 315 * Check if a standard page should be considered dynamic, thus not cached. 316 * i.e. Standard page with commerce element, we want to keep commerce data up to date, 317 * So we can't allow odysseus to cache the page, serving outdated commerce data. 318 * 319 * @return bool 320 */ 321 private function isDynamicStandardPage() 322 { 323 if ($this->isDynamicRoute(ltrim($this->request['path'], '/'))) { 324 // page containing commerce element is considered dynamic page here, 325 // so odysseus don't cache it. 326 // so commerce data can stay up to date. 327 328 // we add a isDynamic in request. 329 // it will tell DeployedServiceController to return in dynamic page's format instead. 330 $this->request['isDynamic'] = true; 331 return true; 332 } 333 return false; 334 } 335 336 /** 337 * Determines if the first directory in the request is a known "dynamic" endpoint 338 * 339 * @param string $directory 340 * 341 * @return bool 342 */ 343 private function isDynamicRoute( $directory ) 344 { 345 if (starts_with_any($directory, array('store', 'blog', 'apps', 'gdpr', '.well-known'))) { 346 return true; 347 } 348 349 return (isset($this->site['dynamic']) && isset($this->site['dynamic'][$directory])); 350 } 351 352 /** 353 * Determines if the current page (by filename) is in the published site data hierarchy 354 * 355 * @param string $page 356 * 357 * @return bool 358 */ 359 private function isPageInPublishedData( $page ) 360 { 361 if ( isset( $page ) === false ) 362 { 363 $page = $this->request['file']; 364 } 365 366 return in_array( $page, $this->site['pages'] ); 367 } 368 369 /** 370 * Determines if the current request is a client API related request 371 * 372 * @return bool 373 */ 374 private function isClientApiRequest( ) 375 { 376 if ( strpos( $this->request['path'], '/ajax/' ) === 0 ) 377 { 378 return true; 379 } 380 381 if (strpos($this->request['path'], '/app/store/api/') === 0) { 382 return true; 383 } 384 385 return false; 386 } 387 388 /** 389 * Attempts to retrieve the HTTP headers from the current request 390 * 391 * @return array|bool 392 */ 393 private static function getHeaders( ) 394 { 395 if ( \function_exists( 'apache_request_headers' ) === true ) 396 { 397 return \apache_request_headers( ); 398 } 399 400 foreach ( $_SERVER as $key => $value ) 401 { 402 if ( substr( $key, 0, 5 ) === 'HTTP_' ) 403 { 404 $headers[str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', substr( $key, 5 ) ) ) ) )] = $value; 405 } 406 elseif ( $key === 'CONTENT_TYPE' || $key === 'CONTENT_LENGTH' ) 407 { 408 $headers[str_replace( '_', '-', ucwords( strtolower( $key ) ) )] = $value; 409 } 410 } 411 412 if ( isset( $headers ) === true ) 413 { 414 return $headers; 415 } 416 417 return false; 418 } 419 420 /** 421 * Output some content if the page has redirect header. 422 * 423 * This is used to prevent some FTP (i.e. fatcow.com) has firewall not allow empty content redirect header. 424 * 425 */ 426 private function finalizeOutput() 427 { 428 if ($this->isRedirect === true) { 429 // this shouldn't appear, as redirect header would take care of it. 430 // in case it's seen, reload link would help user to manually reload/redirect the page. 431 echo "<a onclick='location.reload()'>click here to reload the page.</a>"; 432 } 433 } 434} 435 436$handler = new Handler( $_REQUEST );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.