__('Page View', __FILE__), // getModuleInfo title 'summary' => __('All page views are routed through this Process', __FILE__), // getModuleInfo summary 'version' => 101, 'permanent' => true, 'permission' => 'page-view', ); } // public static $n = 0; /** * URL that should be redirected to for this request * * Set by other methods in this class, and checked by the execute method before rendering. * */ protected $redirectURL = ''; /** * Sanitized URL that generated this request * * Set by the getPage() method and passed to the pageNotFound function. * */ protected $requestURL = ''; /** * Requested filename, if URL in /path/to/page/-/filename.ext format * */ protected $requestFile = ''; /** * Prefix for page numbers in URLs * */ protected $pageNumUrlPrefix = 'page'; /** * Retrieve a page, check access, and render * */ public function ___execute() { if($this->config->pageNumUrlPrefix) $this->pageNumUrlPrefix = $this->config->pageNumUrlPrefix; $this->pages->setOutputFormatting(true); $page = $this->getPage(); if($page && $page->id) { $page->setOutputFormatting(true); $_page = $page; $page = $this->checkAccess($page); if(!$page) return $this->pageNotFound($_page, $this->requestURL); $this->checkProtocol($page); if($this->redirectURL) $this->session->redirect($this->redirectURL); $this->setFuel('page', $page); $this->ready(); try { if($this->requestFile) $this->sendFile($page, $this->requestFile); else return $page->render(); } catch(Wire404Exception $e) { return $this->pageNotFound($page, $this->requestURL); } } else { return $this->pageNotFound(new NullPage(), $this->requestURL); } } /** * Hook called when the $page API var is ready, and before the $page is rendered. * */ public function ___ready() { $this->modules->triggerReady(); } /** * Hook called with the pageview has been finished and output has been sent. Note this is called in /index.php. * */ public function ___finished() { } /** * Hook called when the pageview failed to finish due to an exception. * * Sends a copy of the exception that occurred. * */ public function ___failed(Exception $e) { } /** * Get the requested page and populate it with identified urlSegments or page numbers * * @return Page|null * */ protected function getPage() { $it = isset($_GET['it']) ? $_GET['it'] : "/"; unset($_GET['it']); $it = preg_replace('{[^-_./a-zA-Z0-9]}', '', $it); if($it[0] != '/') $it = "/$it"; // check for secured filename $filePrefix = wire('config')->pagefileUrlPrefix; if($filePrefix && strpos($it, '/' . $filePrefix) !== false) { if(preg_match('{^(.*/)' . $filePrefix . '([-_.a-zA-Z0-9]+)$}', $it, $matches) && strpos($matches[2], '.')) { $it = $matches[1]; $this->requestFile = $matches[2]; } } // optimization to filter out page numbers first if(strpos($it, '/' . $this->pageNumUrlPrefix) !== false && preg_match('{/' . $this->pageNumUrlPrefix . '\d+/?$}', $it)) { // URL contains a page number, but we'll let it be handled by the checkUrlSegments function later $page = null; } else { $page = $this->pages->get("path=$it, status<" . Page::statusMax); } $hasTrailingSlash = substr($it, -1) == '/'; if($page && $page->id) { // trailing slash vs. non trailing slash, enforced if not homepage // redirect to proper trailed slash version if incorrect version is present. $s = $page->template->slashUrls; if($page->id > 1 && ((!$hasTrailingSlash && $s !== 0) || ($hasTrailingSlash && $s === 0))) { $this->redirectURL = $page->url; } return $page; } $this->requestURL = $it; $urlSegments = array(); $maxSegments = wire('config')->maxUrlSegments; if(is_null($maxSegments)) $maxSegments = 4; // default $cnt = 0; // if the page isn't found, then check if a page one path level before exists // this loop allows for us to have both a urlSegment and a pageNum while((!$page || !$page->id) && $cnt < $maxSegments) { $it = rtrim($it, '/'); $pos = strrpos($it, '/')+1; $urlSegment = substr($it, $pos); $urlSegments[$cnt] = $urlSegment; $it = substr($it, 0, $pos); // $it no longer includes the urlSegment $page = $this->pages->get("path=$it, status<" . Page::statusMax); $cnt++; } // if we still found no page, then we can abort if(!$page || !$page->id) return null; // if URL segments and/or page numbers are present and not allowed then abort if(!$this->checkUrlSegments($urlSegments, $page)) return null; return $page; } /** * Identify and populate URL segments and page numbers * * @param array $urlSegments URL segments as found in getPage() * @param Page $page * @return bool Returns false if URL segments found and aren't allowed * */ protected function checkUrlSegments(array $urlSegments, Page $page) { if(!count($urlSegments)) return true; $lastSegment = reset($urlSegments); $urlSegments = array_reverse($urlSegments); $pageNumUrlPrefix = $this->pageNumUrlPrefix; // check if the last urlSegment is setting a page number and that page numbers are allowed if(strpos($lastSegment, $pageNumUrlPrefix) === 0 && strlen($lastSegment) > strlen($pageNumUrlPrefix) && $page->template->allowPageNum) { // meets the requirements for a page number: last portion of URL and starts with 'page' $pageNum = substr($lastSegment, strlen($pageNumUrlPrefix)); // now check to see if it also ends with digits if(ctype_digit("$pageNum")) { $pageNum = (int) $pageNum; if($pageNum > self::maxPageNum) return false; $page->pageNum = $pageNum; // backwards compatibility $this->input->setPageNum($pageNum); array_pop($urlSegments); } } // return false if URL segments aren't allowed with this page template if($page->template != 'admin' && count($urlSegments) && !$page->template->urlSegments) return false; // now set the URL segments to the $input API variable $cnt = 1; foreach($urlSegments as $urlSegment) { if($cnt == 1) $page->urlSegment = $urlSegment; // backwards compatibility $this->input->setUrlSegment($cnt, $urlSegment); $cnt++; } return true; } /** * Check that the current user has access to the page and return it * * If the user doesn't have access, then a login Page or NULL (for 404) is returned instead. * * @return Page|null * */ protected function checkAccess($page) { if($page->viewable()) return $page; if($this->requestFile) { // if a file was requested, we still allow view even if page doesn't have template file // this is something that viewable() does not echeck if($page->editable()) return $page; if($page->status < Page::statusUnpublished && wire('user')->hasPermission('page-view', $page)) return $page; } $redirectLogin = $page->getAccessTemplate()->redirectLogin; if($redirectLogin) { if(ctype_digit("$redirectLogin")) { $redirectLogin = (int) $redirectLogin; if($redirectLogin == 1) $redirectLogin = $this->config->loginPageID; //$this->error("You don't have permission to access this page"); $page = $this->pages->get($redirectLogin); } else { $redirectLogin = str_replace('{id}', $page->id, $redirectLogin); $this->redirectURL = $redirectLogin; } } else { $page = null; } return $page; } /** * If the template requires a different protocol than what is here, then redirect to it. * * This method just silently sets the $this->redirectURL var if a redirect is needed. * Note this does not work if GET vars are present in the URL -- they will be lost in the redirect. * * @param Page $page * */ protected function checkProtocol($page) { if(!$page->template->https) return; $url = $this->config->httpHost . $page->url; if($page->urlSegment) $url .= $page->urlSegment . '/'; if($page->pageNum > 1) { $prefix = $this->config->pageNumUrlPrefix ? $this->config->pageNumUrlPrefix : 'page'; $url .= "$prefix{$page->pageNum}"; } if($page->template->https == -1 && $this->config->https) { // redirect to HTTP non-secure version $this->redirectURL = "http://$url"; } else if($page->template->https == 1 && !$this->config->https) { // redirect to HTTPS secure version $this->redirectURL = "https://$url"; } } /** * Passthru a file for a non-public page * * If the page is public, then it just does a 301 redirect to the file. * */ protected function ___sendFile($page, $basename) { $filename = $page->filesManager->path() . $basename; if(!is_file($filename)) throw new Wire404Exception('File not found'); if($page->isPublic()) { wire('session')->redirect($page->filesManager->url() . $basename); } else { $options = array('exit' => false); wireSendFile($filename, $options); } } /** * Called when a page is not found, sends 404 header, and displays the configured 404 page instead. * * Method is hookable, for instance if you wanted to log 404s. * * @param Page|null $page Page that was found if applicable (like if user didn't have permission or $page's template threw the 404) * If not applicable then NULL will be given instead. * @param string $url The URL that the request originated from (like $_SERVER['REQUEST_URI'] but already sanitized) * */ protected function ___pageNotFound($page, $url) { $config = $this->config; // check if the request was to a /site/assets/files/123/filename.jpg that may have gone from public to not if(strpos($config->urls->root . ltrim($url, '/'), $config->urls->files) !== false) { if($this->pagefileNotFound($url)) return; } header("HTTP/1.1 404 Page Not Found"); if($config->http404PageID) { $page = $this->pages->get($config->http404PageID); if(!$page) throw new WireException("config::http404PageID does not exist - please check your config"); $this->setFuel('page', $page); return $page->render(); } else { return "404 page not found"; } } /** * Handle request for a Pagefile that was not found * * In this case, we look for a request to a Pagefile URL that may have gone from public to private * and set setup the appropriate temporary redirect so that any links continue working, so long * as the user has access. * * @param string $url * @return bool Whether or not this function handled the request * */ protected function ___pagefileNotFound($url) { if($this->user->isGuest()) return false; if(!preg_match('{/(\d+)/([-_.a-zA-Z0-9]+)$}', $url, $matches)) return false; $page = wire('pages')->get((int) $matches[1]); if(!$page->id || $page->isPublic() || !$page->viewable()) return false; // issue a temporary redirect to the protected filename $this->session->redirect($page->url . $this->config->pagefileUrlPrefix . $matches[2], false); return true; } }