MobileComponent.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. <?php
  2. App::uses('Component', 'Controller');
  3. App::uses('Router', 'Routing');
  4. /**
  5. * A component to easily serve mobile views to users.
  6. * It allows good default values while not being restrictive as you can always
  7. * overwrite the auto-detection manually to force desktop or mobile version.
  8. *
  9. * Uses Session to remember lookups: User.mobile and User.nomobile
  10. * - mobile is the auto-detection (true/false)
  11. * - nomobile can be set by the user and overrides the default behavior/detection
  12. * (1=true/0=false or -1=null which will remove the override)
  13. * Uses object attributes as well as Configure to store the results for later use.
  14. * It also pushes switch urls to the view.
  15. *
  16. * New:
  17. * - Support for named params has been dropped in favor of query strings.
  18. * - Support for different engines (vendor should be used as this is up to date).
  19. * - Allows Configure setup and auto-start for easy default cases.
  20. * - Accept closures to easily use any custom detection engine.
  21. * - Cleanup and tests
  22. *
  23. * @author Mark Scherer
  24. * @license http://opensource.org/licenses/mit-license.php MIT
  25. */
  26. class MobileComponent extends Component {
  27. public $components = ['Session'];
  28. public $Controller = null;
  29. /**
  30. * Stores the result of the auto-detection.
  31. *
  32. * @var bool
  33. */
  34. public $isMobile = null;
  35. /**
  36. * Stores the final detection result including user preference.
  37. *
  38. * @var bool
  39. */
  40. public $setMobile = null;
  41. /**
  42. * Default values. Can also be set using Configure.
  43. *
  44. * @param array
  45. */
  46. protected $_defaultConfig = [
  47. 'on' => 'startup', // initialize (prior to controller's beforeRender) or startup
  48. 'engine' => 'vendor', // cake internal (deprecated), tools (deprecated) or vendor
  49. 'themed' => false, // If false uses subfolders instead of themes: /View/.../mobile/
  50. 'mobile' => ['mobile', 'tablet'], // what is mobile? tablets as well? only for vendor
  51. 'auto' => false, // auto set mobile views
  52. ];
  53. /**
  54. * MobileComponent::__construct()
  55. *
  56. * @param ComponentCollection $collection
  57. * @param array $config
  58. */
  59. public function __construct(ComponentCollection $collection, $config = []) {
  60. $defaults = (array)Configure::read('Mobile') + $this->_defaultConfig;
  61. $config += $defaults;
  62. parent::__construct($collection, $config);
  63. }
  64. /**
  65. * MobileComponent::initialize()
  66. *
  67. * @param Controller $Controller
  68. * @return void
  69. */
  70. public function initialize(Controller $Controller) {
  71. parent::initialize($Controller);
  72. $this->Controller = $Controller;
  73. if ($this->settings['on'] !== 'initialize') {
  74. return;
  75. }
  76. $this->_init();
  77. }
  78. /**
  79. * MobileComponent::startup()
  80. *
  81. * @param Controller $Controller
  82. * @return void
  83. */
  84. public function startup(Controller $Controller) {
  85. parent::startup($Controller);
  86. if ($this->settings['on'] !== 'startup') {
  87. return;
  88. }
  89. $this->_init();
  90. }
  91. /**
  92. * Main auto-detection logic including session based storage to avoid
  93. * multiple lookups.
  94. *
  95. * Uses "mobile" query string to overwrite the auto-detection.
  96. * -1 clears the fixation
  97. * 1 forces mobile
  98. * 0 forces no-mobile
  99. *
  100. * @return void
  101. */
  102. protected function _init() {
  103. $mobileOverwrite = $this->Controller->request->query('mobile');
  104. if ($mobileOverwrite !== null) {
  105. if ($mobileOverwrite === '-1') {
  106. $noMobile = null;
  107. } else {
  108. $wantsMobile = (bool)$mobileOverwrite;
  109. $noMobile = (int)(!$wantsMobile);
  110. }
  111. $this->Session->write('User.nomobile', $noMobile);
  112. }
  113. $this->isMobile();
  114. if (!$this->settings['auto']) {
  115. return;
  116. }
  117. $this->setMobile();
  118. }
  119. /**
  120. * Serve mobile views if available
  121. *
  122. * can be called from beforeFilter() to automatically serve an alternative mobile view
  123. * if the file exists. If it doesn't exist in `/View/[ViewPath]/mobile/` the normal one
  124. * will be used.
  125. *
  126. * @deprecated in favor of themed solution?
  127. * @return void
  128. */
  129. public function serveMobileIfAvailable() {
  130. $viewDir = App::path('View');
  131. // returns an array
  132. /*
  133. * array(
  134. * (int) 0 => '/var/www/maps-cakephp2/app/View/'
  135. * )
  136. */
  137. $mobileViewFile = $viewDir[0] . $this->viewPath . DS . 'Mobile' . DS . $this->params['action'] . '.ctp';
  138. //Debugger::log($this->viewPath);
  139. // use this to log the output to
  140. // app/tmp/logs/debug.log
  141. if (file_exists($mobileViewFile)) {
  142. // if device is mobile, change layout to mobile
  143. // but only if a view exists for it.
  144. $this->layout = 'mobile';
  145. // and if a mobile view file has been
  146. // created for the action, serve it instead
  147. // of the default view file
  148. $this->viewPath = $this->viewPath . '/Mobile/';
  149. }
  150. }
  151. /**
  152. * Sets mobile views as `Mobile` theme.
  153. *
  154. * Only needs to be called if auto is set to false.
  155. * Then you probably want to call this from your AppController::beforeRender().
  156. *
  157. * @return void
  158. */
  159. public function setMobile() {
  160. if ($this->isMobile === null) {
  161. $this->isMobile();
  162. }
  163. $noMobile = $this->Session->read('User.nomobile');
  164. if (!$this->isMobile && $noMobile === null || $noMobile) {
  165. $this->setMobile = false;
  166. } else {
  167. $this->setMobile = true;
  168. }
  169. $urlParams = Router::getParams(true);
  170. if (!isset($urlParams['named'])) {
  171. $urlParams['named'] = [];
  172. }
  173. if (!isset($urlParams['pass'])) {
  174. $urlParams['pass'] = [];
  175. }
  176. $urlParams = array_merge($urlParams, $urlParams['named'], $urlParams['pass']);
  177. unset($urlParams['named']);
  178. unset($urlParams['pass']);
  179. if (isset($urlParams['prefix'])) {
  180. unset($urlParams['prefix']);
  181. }
  182. if ($this->setMobile) {
  183. $urlParams['?']['mobile'] = 0;
  184. $url = Router::url($urlParams);
  185. $this->Controller->set('desktopUrl', $url);
  186. } else {
  187. $urlParams['?']['mobile'] = 1;
  188. $url = Router::url($urlParams);
  189. $this->Controller->set('mobileUrl', $url);
  190. }
  191. Configure::write('User.isMobile', (int)$this->isMobile);
  192. Configure::write('User.setMobile', (int)$this->setMobile);
  193. if (!$this->setMobile) {
  194. return;
  195. }
  196. if (!$this->settings['themed']) {
  197. $this->serveMobileIfAvailable();
  198. return;
  199. }
  200. $this->Controller->viewClass = 'Theme';
  201. $this->Controller->theme = 'Mobile';
  202. }
  203. /**
  204. * Determines if we need to so serve mobile views based on session preference
  205. * and browser headers.
  206. *
  207. * @return bool Success
  208. */
  209. public function isMobile() {
  210. if ($this->isMobile !== null) {
  211. return $this->isMobile;
  212. }
  213. $this->isMobile = $this->Session->read('User.mobile');
  214. if ($this->isMobile !== null) {
  215. return $this->isMobile;
  216. }
  217. $this->isMobile = (bool)$this->detect();
  218. $this->Session->write('User.mobile', (int)$this->isMobile);
  219. return $this->isMobile;
  220. }
  221. /**
  222. * Detects if the current request is from a mobile device.
  223. *
  224. * Note that the cake internal way might soon be deprecated:
  225. * https://github.com/cakephp/cakephp/issues/2546
  226. *
  227. * @return bool Success
  228. */
  229. public function detect() {
  230. // Deprecated - the vendor libs are far more accurate and up to date
  231. if ($this->settings['engine'] === 'cake') {
  232. $this->Controller->request->addDetector('mobile', ['options' => ['OMNIA7']]);
  233. return $this->Controller->request->is('mobile');
  234. }
  235. if (is_callable($this->settings['engine'])) {
  236. return call_user_func($this->settings['engine']);
  237. }
  238. if (!in_array($this->settings['engine'], ['tools', 'vendor'])) {
  239. throw new CakeException(sprintf('Engine %s not available', $this->settings['engine']));
  240. }
  241. return $this->detectByVendor($this->settings['engine']);
  242. }
  243. /**
  244. * Simple auto-detection based on Tools plugin or vendor classes.
  245. *
  246. * @param string $engine
  247. * @return bool Success
  248. */
  249. public function detectByVendor($engine) {
  250. $isMobile = $this->Session->read('Session.mobile');
  251. if ($isMobile !== null) {
  252. return (bool)$isMobile;
  253. }
  254. // Deprecated - the vendor libs are far more accurate and up to date
  255. if ($engine === 'tools') {
  256. App::uses('UserAgentLib', 'Tools.Lib');
  257. $UserAgentLib = new UserAgentLib();
  258. return (bool)$UserAgentLib->isMobile();
  259. }
  260. App::import('Vendor', 'Tools.MobileDetect', ['file' => 'MobileDetect/Mobile_Detect.php']);
  261. $MobileDetect = new Mobile_Detect();
  262. $result = empty($this->settings['mobile']) ? 0 : 1;
  263. if (in_array('mobile', $this->settings['mobile'])) {
  264. $result &= $MobileDetect->isMobile();
  265. }
  266. if (in_array('tablet', $this->settings['mobile'])) {
  267. $result |= $MobileDetect->isTablet();
  268. } else {
  269. $result &= !$MobileDetect->isTablet();
  270. }
  271. $this->Session->write('Session.mobile', (bool)$result);
  272. return (bool)$result;
  273. }
  274. }