printgcp.modules.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. <?php
  2. /*
  3. * Copyright (C) 2014-2019 Frédéric France <frederic.france@netlogic.fr>
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. * or see https://www.gnu.org/
  18. */
  19. /**
  20. * \file htdocs/core/modules/printing/printgcp.modules.php
  21. * \ingroup printing
  22. * \brief File to provide printing with Google Cloud Print
  23. */
  24. include_once DOL_DOCUMENT_ROOT.'/core/modules/printing/modules_printing.php';
  25. require_once DOL_DOCUMENT_ROOT.'/includes/OAuth/bootstrap.php';
  26. use OAuth\Common\Storage\DoliStorage;
  27. use OAuth\Common\Consumer\Credentials;
  28. use OAuth\OAuth2\Service\Google;
  29. /**
  30. * Class to provide printing with Google Cloud Print
  31. */
  32. class printing_printgcp extends PrintingDriver
  33. {
  34. /**
  35. * @var string module name
  36. */
  37. public $name = 'printgcp';
  38. /**
  39. * @var string module description
  40. */
  41. public $desc = 'PrintGCPDesc';
  42. /**
  43. * @var string String with name of icon for myobject. Must be the part after the 'object_' into object_myobject.png
  44. */
  45. public $picto = 'printer';
  46. /**
  47. * @var string module description
  48. */
  49. public $active = 'PRINTING_PRINTGCP';
  50. /**
  51. * @var array module parameters
  52. */
  53. public $conf = array();
  54. /**
  55. * @var string google id
  56. */
  57. public $google_id = '';
  58. /**
  59. * @var string google secret
  60. */
  61. public $google_secret = '';
  62. /**
  63. * @var string Error code (or message)
  64. */
  65. public $error = '';
  66. /**
  67. * @var string[] Error codes (or messages)
  68. */
  69. public $errors = array();
  70. /**
  71. * @var DoliDB Database handler.
  72. */
  73. public $db;
  74. private $OAUTH_SERVICENAME_GOOGLE = 'Google';
  75. const LOGIN_URL = 'https://accounts.google.com/o/oauth2/token';
  76. const PRINTERS_SEARCH_URL = 'https://www.google.com/cloudprint/search';
  77. const PRINTERS_GET_JOBS = 'https://www.google.com/cloudprint/jobs';
  78. const PRINT_URL = 'https://www.google.com/cloudprint/submit';
  79. const LANGFILE = 'printgcp';
  80. /**
  81. * Constructor
  82. *
  83. * @param DoliDB $db Database handler
  84. */
  85. public function __construct($db)
  86. {
  87. global $conf, $langs, $dolibarr_main_url_root;
  88. // Define $urlwithroot
  89. $urlwithouturlroot = preg_replace('/'.preg_quote(DOL_URL_ROOT, '/').'$/i', '', trim($dolibarr_main_url_root));
  90. $urlwithroot = $urlwithouturlroot.DOL_URL_ROOT; // This is to use external domain name found into config file
  91. //$urlwithroot=DOL_MAIN_URL_ROOT; // This is to use same domain name than current
  92. $this->db = $db;
  93. if (!$conf->oauth->enabled) {
  94. $this->conf[] = array(
  95. 'varname'=>'PRINTGCP_INFO',
  96. 'info'=>$langs->transnoentitiesnoconv("WarningModuleNotActive", "OAuth"),
  97. 'type'=>'info',
  98. );
  99. } else {
  100. $this->google_id = $conf->global->OAUTH_GOOGLE_ID;
  101. $this->google_secret = $conf->global->OAUTH_GOOGLE_SECRET;
  102. // Token storage
  103. $storage = new DoliStorage($this->db, $this->conf);
  104. //$storage->clearToken($this->OAUTH_SERVICENAME_GOOGLE);
  105. // Setup the credentials for the requests
  106. $credentials = new Credentials(
  107. $this->google_id,
  108. $this->google_secret,
  109. $urlwithroot.'/core/modules/oauth/google_oauthcallback.php'
  110. );
  111. $access = ($storage->hasAccessToken($this->OAUTH_SERVICENAME_GOOGLE) ? 'HasAccessToken' : 'NoAccessToken');
  112. $serviceFactory = new \OAuth\ServiceFactory();
  113. $apiService = $serviceFactory->createService($this->OAUTH_SERVICENAME_GOOGLE, $credentials, $storage, array());
  114. $token_ok = true;
  115. try {
  116. $token = $storage->retrieveAccessToken($this->OAUTH_SERVICENAME_GOOGLE);
  117. } catch (Exception $e) {
  118. $this->errors[] = $e->getMessage();
  119. $token_ok = false;
  120. }
  121. //var_dump($this->errors);exit;
  122. $expire = false;
  123. // Is token expired or will token expire in the next 30 seconds
  124. if ($token_ok) {
  125. $expire = ($token->getEndOfLife() !== -9002 && $token->getEndOfLife() !== -9001 && time() > ($token->getEndOfLife() - 30));
  126. }
  127. // Token expired so we refresh it
  128. if ($token_ok && $expire) {
  129. try {
  130. // il faut sauvegarder le refresh token car google ne le donne qu'une seule fois
  131. $refreshtoken = $token->getRefreshToken();
  132. $token = $apiService->refreshAccessToken($token);
  133. $token->setRefreshToken($refreshtoken);
  134. $storage->storeAccessToken($this->OAUTH_SERVICENAME_GOOGLE, $token);
  135. } catch (Exception $e) {
  136. $this->errors[] = $e->getMessage();
  137. }
  138. }
  139. if ($this->google_id != '' && $this->google_secret != '') {
  140. $this->conf[] = array('varname'=>'PRINTGCP_INFO', 'info'=>'GoogleAuthConfigured', 'type'=>'info');
  141. $this->conf[] = array(
  142. 'varname'=>'PRINTGCP_TOKEN_ACCESS',
  143. 'info'=>$access,
  144. 'type'=>'info',
  145. 'renew'=>$urlwithroot.'/core/modules/oauth/google_oauthcallback.php?state=userinfo_email,userinfo_profile,cloud_print&backtourl='.urlencode(DOL_URL_ROOT.'/printing/admin/printing.php?mode=setup&driver=printgcp'),
  146. 'delete'=>($storage->hasAccessToken($this->OAUTH_SERVICENAME_GOOGLE) ? $urlwithroot.'/core/modules/oauth/google_oauthcallback.php?action=delete&token='.newToken().'&backtourl='.urlencode(DOL_URL_ROOT.'/printing/admin/printing.php?mode=setup&driver=printgcp') : '')
  147. );
  148. if ($token_ok) {
  149. $expiredat = '';
  150. $refreshtoken = $token->getRefreshToken();
  151. $endoflife = $token->getEndOfLife();
  152. if ($endoflife == $token::EOL_NEVER_EXPIRES) {
  153. $expiredat = $langs->trans("Never");
  154. } elseif ($endoflife == $token::EOL_UNKNOWN) {
  155. $expiredat = $langs->trans("Unknown");
  156. } else {
  157. $expiredat = dol_print_date($endoflife, "dayhour");
  158. }
  159. $this->conf[] = array('varname'=>'TOKEN_REFRESH', 'info'=>((!empty($refreshtoken)) ? 'Yes' : 'No'), 'type'=>'info');
  160. $this->conf[] = array('varname'=>'TOKEN_EXPIRED', 'info'=>($expire ? 'Yes' : 'No'), 'type'=>'info');
  161. $this->conf[] = array('varname'=>'TOKEN_EXPIRE_AT', 'info'=>($expiredat), 'type'=>'info');
  162. }
  163. /*
  164. if ($storage->hasAccessToken($this->OAUTH_SERVICENAME_GOOGLE)) {
  165. $this->conf[] = array('varname'=>'PRINTGCP_AUTHLINK', 'link'=>$urlwithroot.'/core/modules/oauth/google_oauthcallback.php?backtourl='.urlencode(DOL_URL_ROOT.'/printing/admin/printing.php?mode=setup&driver=printgcp'), 'type'=>'authlink');
  166. $this->conf[] = array('varname'=>'DELETE_TOKEN', 'link'=>$urlwithroot.'/core/modules/oauth/google_oauthcallback.php?action=delete&token='.newToken().'&backtourl='.urlencode(DOL_URL_ROOT.'/printing/admin/printing.php?mode=setup&driver=printgcp'), 'type'=>'delete');
  167. } else {
  168. $this->conf[] = array('varname'=>'PRINTGCP_AUTHLINK', 'link'=>$urlwithroot.'/core/modules/oauth/google_oauthcallback.php?backtourl='.urlencode(DOL_URL_ROOT.'/printing/admin/printing.php?mode=setup&driver=printgcp'), 'type'=>'authlink');
  169. }*/
  170. } else {
  171. $this->conf[] = array('varname'=>'PRINTGCP_INFO', 'info'=>'GoogleAuthNotConfigured', 'type'=>'info');
  172. }
  173. }
  174. // do not display submit button
  175. $this->conf[] = array('enabled'=>0, 'type'=>'submit');
  176. }
  177. /**
  178. * Return list of available printers
  179. *
  180. * @return int 0 if OK, >0 if KO
  181. */
  182. public function listAvailablePrinters()
  183. {
  184. global $conf, $langs;
  185. $error = 0;
  186. $langs->load('printing');
  187. $html = '<tr class="liste_titre">';
  188. $html .= '<td>'.$langs->trans('GCP_Name').'</td>';
  189. $html .= '<td>'.$langs->trans('GCP_displayName').'</td>';
  190. $html .= '<td>'.$langs->trans('GCP_Id').'</td>';
  191. $html .= '<td>'.$langs->trans('GCP_OwnerName').'</td>';
  192. $html .= '<td>'.$langs->trans('GCP_State').'</td>';
  193. $html .= '<td>'.$langs->trans('GCP_connectionStatus').'</td>';
  194. $html .= '<td>'.$langs->trans('GCP_Type').'</td>';
  195. $html .= '<td class="center">'.$langs->trans("Select").'</td>';
  196. $html .= '</tr>'."\n";
  197. $list = $this->getlistAvailablePrinters();
  198. //$html.= '<td><pre>'.print_r($list,true).'</pre></td>';
  199. foreach ($list['available'] as $printer_det) {
  200. $html .= '<tr class="oddeven">';
  201. $html .= '<td>'.$printer_det['name'].'</td>';
  202. $html .= '<td>'.$printer_det['displayName'].'</td>';
  203. $html .= '<td>'.$printer_det['id'].'</td>'; // id to identify printer to use
  204. $html .= '<td>'.$printer_det['ownerName'].'</td>';
  205. $html .= '<td>'.$printer_det['status'].'</td>';
  206. $html .= '<td>'.$langs->trans('STATE_'.$printer_det['connectionStatus']).'</td>';
  207. $html .= '<td>'.$langs->trans('TYPE_'.$printer_det['type']).'</td>';
  208. // Defaut
  209. $html .= '<td class="center">';
  210. if ($conf->global->PRINTING_GCP_DEFAULT == $printer_det['id']) {
  211. $html .= img_picto($langs->trans("Default"), 'on');
  212. } else {
  213. $html .= '<a href="'.$_SERVER["PHP_SELF"].'?action=setvalue&token='.newToken().'&mode=test&varname=PRINTING_GCP_DEFAULT&driver=printgcp&value='.urlencode($printer_det['id']).'" alt="'.$langs->trans("Default").'">'.img_picto($langs->trans("Disabled"), 'off').'</a>';
  214. }
  215. $html .= '</td>';
  216. $html .= '</tr>'."\n";
  217. }
  218. $this->resprint = $html;
  219. return $error;
  220. }
  221. /**
  222. * Return list of available printers
  223. *
  224. * @return array list of printers
  225. */
  226. public function getlistAvailablePrinters()
  227. {
  228. $ret = array();
  229. // Token storage
  230. $storage = new DoliStorage($this->db, $this->conf);
  231. // Setup the credentials for the requests
  232. $credentials = new Credentials(
  233. $this->google_id,
  234. $this->google_secret,
  235. DOL_MAIN_URL_ROOT.'/core/modules/oauth/google_oauthcallback.php'
  236. );
  237. $serviceFactory = new \OAuth\ServiceFactory();
  238. $apiService = $serviceFactory->createService($this->OAUTH_SERVICENAME_GOOGLE, $credentials, $storage, array());
  239. // Check if we have auth token
  240. $token_ok = true;
  241. try {
  242. $token = $storage->retrieveAccessToken($this->OAUTH_SERVICENAME_GOOGLE);
  243. } catch (Exception $e) {
  244. $this->errors[] = $e->getMessage();
  245. $token_ok = false;
  246. }
  247. $expire = false;
  248. // Is token expired or will token expire in the next 30 seconds
  249. if ($token_ok) {
  250. $expire = ($token->getEndOfLife() !== -9002 && $token->getEndOfLife() !== -9001 && time() > ($token->getEndOfLife() - 30));
  251. }
  252. // Token expired so we refresh it
  253. if ($token_ok && $expire) {
  254. try {
  255. // il faut sauvegarder le refresh token car google ne le donne qu'une seule fois
  256. $refreshtoken = $token->getRefreshToken();
  257. $token = $apiService->refreshAccessToken($token);
  258. $token->setRefreshToken($refreshtoken);
  259. $storage->storeAccessToken($this->OAUTH_SERVICENAME_GOOGLE, $token);
  260. } catch (Exception $e) {
  261. $this->errors[] = $e->getMessage();
  262. }
  263. }
  264. // Send a request with api
  265. try {
  266. $response = $apiService->request(self::PRINTERS_SEARCH_URL);
  267. } catch (Exception $e) {
  268. $this->errors[] = $e->getMessage();
  269. print '<pre>'.print_r($e->getMessage(), true).'</pre>';
  270. }
  271. //print '<tr><td><pre>'.print_r($response, true).'</pre></td></tr>';
  272. $responsedata = json_decode($response, true);
  273. $printers = $responsedata['printers'];
  274. // Check if we have printers?
  275. if (is_array($printers) && count($printers) == 0) {
  276. // We dont have printers so return blank array
  277. $ret['available'] = array();
  278. } else {
  279. // We have printers so returns printers as array
  280. $ret['available'] = $printers;
  281. }
  282. return $ret;
  283. }
  284. /**
  285. * Print selected file
  286. *
  287. * @param string $file file
  288. * @param string $module module
  289. * @param string $subdir subdir for file
  290. * @return int 0 if OK, >0 if KO
  291. */
  292. public function printFile($file, $module, $subdir = '')
  293. {
  294. require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
  295. global $conf, $user;
  296. $error = 0;
  297. $fileprint = $conf->{$module}->dir_output;
  298. if ($subdir != '') {
  299. $fileprint .= '/'.$subdir;
  300. }
  301. $fileprint .= '/'.$file;
  302. $mimetype = dol_mimetype($fileprint);
  303. // select printer uri for module order, propal,...
  304. $sql = "SELECT rowid, printer_id, copy FROM ".MAIN_DB_PREFIX."printing WHERE module='".$this->db->escape($module)."' AND driver='printgcp' AND userid=".((int) $user->id);
  305. $result = $this->db->query($sql);
  306. if ($result) {
  307. $obj = $this->db->fetch_object($result);
  308. if ($obj) {
  309. $printer_id = $obj->printer_id;
  310. } else {
  311. if (!empty($conf->global->PRINTING_GCP_DEFAULT)) {
  312. $printer_id = $conf->global->PRINTING_GCP_DEFAULT;
  313. } else {
  314. $this->errors[] = 'NoDefaultPrinterDefined';
  315. $error++;
  316. return $error;
  317. }
  318. }
  319. } else {
  320. dol_print_error($this->db);
  321. }
  322. $ret = $this->sendPrintToPrinter($printer_id, $file, $fileprint, $mimetype);
  323. $this->error = 'PRINTGCP: '.$ret['errormessage'];
  324. if ($ret['status'] != 1) {
  325. $error++;
  326. }
  327. return $error;
  328. }
  329. /**
  330. * Sends document to the printer
  331. *
  332. * @param string $printerid Printer id returned by Google Cloud Print
  333. * @param string $printjobtitle Job Title
  334. * @param string $filepath File Path to be send to Google Cloud Print
  335. * @param string $contenttype File content type by example application/pdf, image/png
  336. * @return array status array
  337. */
  338. public function sendPrintToPrinter($printerid, $printjobtitle, $filepath, $contenttype)
  339. {
  340. // Check if printer id
  341. if (empty($printerid)) {
  342. return array('status' =>0, 'errorcode' =>'', 'errormessage'=>'No provided printer ID');
  343. }
  344. // Open the file which needs to be print
  345. $handle = fopen($filepath, "rb");
  346. if (!$handle) {
  347. return array('status' =>0, 'errorcode' =>'', 'errormessage'=>'Could not read the file.');
  348. }
  349. // Read file content
  350. $contents = fread($handle, filesize($filepath));
  351. fclose($handle);
  352. // Prepare post fields for sending print
  353. $post_fields = array(
  354. 'printerid' => $printerid,
  355. 'title' => $printjobtitle,
  356. 'contentTransferEncoding' => 'base64',
  357. 'content' => base64_encode($contents), // encode file content as base64
  358. 'contentType' => $contenttype,
  359. );
  360. // Dolibarr Token storage
  361. $storage = new DoliStorage($this->db, $this->conf);
  362. // Setup the credentials for the requests
  363. $credentials = new Credentials(
  364. $this->google_id,
  365. $this->google_secret,
  366. DOL_MAIN_URL_ROOT.'/core/modules/oauth/google_oauthcallback.php?service=google'
  367. );
  368. $serviceFactory = new \OAuth\ServiceFactory();
  369. $apiService = $serviceFactory->createService($this->OAUTH_SERVICENAME_GOOGLE, $credentials, $storage, array());
  370. // Check if we have auth token and refresh it
  371. $token_ok = true;
  372. try {
  373. $token = $storage->retrieveAccessToken($this->OAUTH_SERVICENAME_GOOGLE);
  374. } catch (Exception $e) {
  375. $this->errors[] = $e->getMessage();
  376. $token_ok = false;
  377. }
  378. if ($token_ok) {
  379. try {
  380. // il faut sauvegarder le refresh token car google ne le donne qu'une seule fois
  381. $refreshtoken = $token->getRefreshToken();
  382. $token = $apiService->refreshAccessToken($token);
  383. $token->setRefreshToken($refreshtoken);
  384. $storage->storeAccessToken($this->OAUTH_SERVICENAME_GOOGLE, $token);
  385. } catch (Exception $e) {
  386. $this->errors[] = $e->getMessage();
  387. }
  388. }
  389. // Send a request with api
  390. $response = json_decode($apiService->request(self::PRINT_URL, 'POST', $post_fields), true);
  391. //print '<tr><td><pre>'.print_r($response, true).'</pre></td></tr>';
  392. return array('status' => $response['success'], 'errorcode' => $response['errorCode'], 'errormessage' => $response['message']);
  393. }
  394. /**
  395. * List jobs print
  396. *
  397. * @return int 0 if OK, >0 if KO
  398. */
  399. public function listJobs()
  400. {
  401. global $conf, $langs;
  402. $error = 0;
  403. $html = '';
  404. // Token storage
  405. $storage = new DoliStorage($this->db, $this->conf);
  406. // Setup the credentials for the requests
  407. $credentials = new Credentials(
  408. $this->google_id,
  409. $this->google_secret,
  410. DOL_MAIN_URL_ROOT.'/core/modules/oauth/google_oauthcallback.php'
  411. );
  412. $serviceFactory = new \OAuth\ServiceFactory();
  413. $apiService = $serviceFactory->createService($this->OAUTH_SERVICENAME_GOOGLE, $credentials, $storage, array());
  414. // Check if we have auth token
  415. $token_ok = true;
  416. try {
  417. $token = $storage->retrieveAccessToken($this->OAUTH_SERVICENAME_GOOGLE);
  418. } catch (Exception $e) {
  419. $this->errors[] = $e->getMessage();
  420. $token_ok = false;
  421. $error++;
  422. }
  423. $expire = false;
  424. // Is token expired or will token expire in the next 30 seconds
  425. if ($token_ok) {
  426. $expire = ($token->getEndOfLife() !== -9002 && $token->getEndOfLife() !== -9001 && time() > ($token->getEndOfLife() - 30));
  427. }
  428. // Token expired so we refresh it
  429. if ($token_ok && $expire) {
  430. try {
  431. // il faut sauvegarder le refresh token car google ne le donne qu'une seule fois
  432. $refreshtoken = $token->getRefreshToken();
  433. $token = $apiService->refreshAccessToken($token);
  434. $token->setRefreshToken($refreshtoken);
  435. $storage->storeAccessToken($this->OAUTH_SERVICENAME_GOOGLE, $token);
  436. } catch (Exception $e) {
  437. $this->errors[] = $e->getMessage();
  438. $error++;
  439. }
  440. }
  441. // Getting Jobs
  442. // Send a request with api
  443. try {
  444. $response = $apiService->request(self::PRINTERS_GET_JOBS);
  445. } catch (Exception $e) {
  446. $this->errors[] = $e->getMessage();
  447. $error++;
  448. }
  449. $responsedata = json_decode($response, true);
  450. //$html .= '<pre>'.print_r($responsedata,true).'</pre>';
  451. $html .= '<div class="div-table-responsive">';
  452. $html .= '<table width="100%" class="noborder">';
  453. $html .= '<tr class="liste_titre">';
  454. $html .= '<td>'.$langs->trans("Id").'</td>';
  455. $html .= '<td>'.$langs->trans("Date").'</td>';
  456. $html .= '<td>'.$langs->trans("Owner").'</td>';
  457. $html .= '<td>'.$langs->trans("Printer").'</td>';
  458. $html .= '<td>'.$langs->trans("Filename").'</td>';
  459. $html .= '<td>'.$langs->trans("Status").'</td>';
  460. $html .= '<td>'.$langs->trans("Cancel").'</td>';
  461. $html .= '</tr>'."\n";
  462. $jobs = $responsedata['jobs'];
  463. //$html .= '<pre>'.print_r($jobs['0'],true).'</pre>';
  464. if (is_array($jobs)) {
  465. foreach ($jobs as $value) {
  466. $html .= '<tr class="oddeven">';
  467. $html .= '<td>'.$value['id'].'</td>';
  468. $dates = dol_print_date((int) substr($value['createTime'], 0, 10), 'dayhour');
  469. $html .= '<td>'.$dates.'</td>';
  470. $html .= '<td>'.$value['ownerId'].'</td>';
  471. $html .= '<td>'.$value['printerName'].'</td>';
  472. $html .= '<td>'.$value['title'].'</td>';
  473. $html .= '<td>'.$value['status'].'</td>';
  474. $html .= '<td>&nbsp;</td>';
  475. $html .= '</tr>';
  476. }
  477. } else {
  478. $html .= '<tr class="oddeven">';
  479. $html .= '<td colspan="7" class="opacitymedium">'.$langs->trans("None").'</td>';
  480. $html .= '</tr>';
  481. }
  482. $html .= '</table>';
  483. $html .= '</div>';
  484. $this->resprint = $html;
  485. return $error;
  486. }
  487. }